diff --git a/.editorconfig b/.editorconfig index d4094b2cf3..eba04ad326 100644 --- a/.editorconfig +++ b/.editorconfig @@ -306,48 +306,6 @@ dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error -########################################## -# StyleCop Field Naming Rules -# Naming rules for fields follow the StyleCop analyzers -# This does not override any rules using disallowed_style above -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers -########################################## - -# All constant fields must be PascalCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md -dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private -dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const -dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning - -# All static readonly fields must be PascalCase -# Ajusted to ignore private fields. -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md -dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected -dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly -dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning - -# No non-private instance fields are allowed -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md -dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected -dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error - -# Local variables must be camelCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md -dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local -dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent - # This rule should never fire. However, it's included for at least two purposes: # First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. # Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). @@ -357,7 +315,6 @@ dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_chec dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error - ########################################## # Other Naming Rules ########################################## diff --git a/.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/.globalconfig b/.globalconfig new file mode 100644 index 0000000000..8342ab4580 --- /dev/null +++ b/.globalconfig @@ -0,0 +1,81 @@ +is_global = true + +########################################## +# StyleCopAnalyzers Settings +########################################## + +# All constant fields must be PascalCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style + +# All static readonly fields must be PascalCase +# Ajusted to ignore private fields. +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style + +# No non-private instance fields are allowed +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style + +# Local variables must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local +dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style + +########################################## +# StyleCopAnalyzers rule severity +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers +########################################## + +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.ReadabilityRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.NamingRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpacingRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = suggestion + +dotnet_diagnostic.SA1636.severity = none # SA1636: File header copyright text should match + +dotnet_diagnostic.SA1503.severity = warning # BracesMustNotBeOmitted +dotnet_diagnostic.SA1117.severity = warning # ParametersMustBeOnSameLineOrSeparateLines +dotnet_diagnostic.SA1116.severity = warning # SplitParametersMustStartOnLineAfterDeclaration +dotnet_diagnostic.SA1122.severity = warning # UseStringEmptyForEmptyStrings +dotnet_diagnostic.SA1028.severity = warning # CodeMustNotContainTrailingWhitespace +dotnet_diagnostic.SA1500.severity = warning # BracesForMultiLineStatementsMustNotShareLine +dotnet_diagnostic.SA1401.severity = warning # FieldsMustBePrivate +dotnet_diagnostic.SA1519.severity = warning # BracesMustNotBeOmittedFromMultiLineChildStatement +dotnet_diagnostic.SA1111.severity = warning # ClosingParenthesisMustBeOnLineOfLastParameter +dotnet_diagnostic.SA1520.severity = warning # UseBracesConsistently +dotnet_diagnostic.SA1407.severity = warning # ArithmeticExpressionsMustDeclarePrecedence +dotnet_diagnostic.SA1400.severity = warning # AccessModifierMustBeDeclared +dotnet_diagnostic.SA1119.severity = warning # StatementMustNotUseUnnecessaryParenthesis +dotnet_diagnostic.SA1649.severity = warning # FileNameMustMatchTypeName +dotnet_diagnostic.SA1121.severity = warning # UseBuiltInTypeAlias +dotnet_diagnostic.SA1132.severity = warning # DoNotCombineFields +dotnet_diagnostic.SA1134.severity = warning # AttributesMustNotShareLine +dotnet_diagnostic.SA1106.severity = warning # CodeMustNotContainEmptyStatements +dotnet_diagnostic.SA1312.severity = warning # VariableNamesMustBeginWithLowerCaseLetter +dotnet_diagnostic.SA1303.severity = warning # ConstFieldNamesMustBeginWithUpperCaseLetter +dotnet_diagnostic.SA1310.severity = warning # FieldNamesMustNotContainUnderscore +dotnet_diagnostic.SA1130.severity = warning # UseLambdaSyntax +dotnet_diagnostic.SA1405.severity = warning # DebugAssertMustProvideMessageText +dotnet_diagnostic.SA1205.severity = warning # PartialElementsMustDeclareAccess +dotnet_diagnostic.SA1306.severity = warning # FieldNamesMustBeginWithLowerCaseLetter +dotnet_diagnostic.SA1209.severity = warning # UsingAliasDirectivesMustBePlacedAfterOtherUsingDirectives +dotnet_diagnostic.SA1216.severity = warning # UsingStaticDirectivesMustBePlacedAtTheCorrectLocation +dotnet_diagnostic.SA1133.severity = warning # DoNotCombineAttributes +dotnet_diagnostic.SA1135.severity = warning # UsingDirectivesMustBeQualified diff --git a/Directory.Build.props b/Directory.Build.props index 74f1ebad3d..fcf605f555 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,12 +2,6 @@ - - + - - - - $(MSBuildThisFileDirectory)codeanalysis.ruleset - diff --git a/build/templates/UmbracoProject/UmbracoProject.csproj b/build/templates/UmbracoProject/UmbracoProject.csproj index ebbaa1bc11..a9bc2f36b6 100644 --- a/build/templates/UmbracoProject/UmbracoProject.csproj +++ b/build/templates/UmbracoProject/UmbracoProject.csproj @@ -12,9 +12,13 @@ - - - + + + + diff --git a/codeanalysis.ruleset b/codeanalysis.ruleset deleted file mode 100644 index ab5ad88f57..0000000000 --- a/codeanalysis.ruleset +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/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..73c5ea18f5 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -1,7 +1,10 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Deploy.Core.Configuration.DeployConfiguration; +using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration; using Umbraco.Forms.Core.Configuration; using SecuritySettings = Umbraco.Cms.Core.Configuration.Models.SecuritySettings; @@ -82,7 +85,12 @@ namespace JsonSchema public BasicAuthSettings BasicAuth { get; set; } public PackageMigrationSettings PackageMigration { get; set; } + public LegacyPasswordMigrationSettings LegacyPasswordMigration { get; set; } + + public ContentDashboardSettings ContentDashboard { get; set; } + + public HelpPageSettings HelpPage { get; set; } } /// @@ -116,6 +124,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 56% rename from src/Umbraco.Core/Configuration/ContentDashboardSettings.cs rename to src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs index 7bef36dba4..768f7c2088 100644 --- a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs @@ -1,6 +1,12 @@ - +using System.ComponentModel; +using Umbraco.Cms.Core.Configuration.Models; + namespace Umbraco.Cms.Core.Configuration { + /// + /// Typed configuration options for content dashboard settings. + /// + [UmbracoOptions(Constants.Configuration.ConfigContentDashboard)] public class ContentDashboardSettings { private const string DefaultContentDashboardPath = "cms"; @@ -18,6 +24,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 efcd9ee1cd..f9922459c3 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,18 +173,18 @@ 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); } diff --git a/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs b/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs new file mode 100644 index 0000000000..3bd518b37e --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Configuration.Models +{ + [UmbracoOptions(Constants.Configuration.ConfigHelpPage)] + public class HelpPageSettings + { + /// + /// Gets or sets the allowed addresses to retrieve data for the content dashboard. + /// + public string[] HelpPageUrlAllowList { get; set; } + } +} diff --git a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs index 26c2b26803..cd62655f58 100644 --- a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs @@ -31,5 +31,10 @@ namespace Umbraco.Cms.Core.Configuration.Models /// true if [debug mode]; otherwise, false. [DefaultValue(StaticDebug)] public bool Debug { get; set; } = StaticDebug; + + /// + /// Gets or sets a value specifying the name of the site. + /// + public string SiteName { get; set; } } } diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs index 540db22670..54db8d42ed 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 System.ComponentModel.DataAnnotations; @@ -17,33 +18,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 = + internal static readonly Umbraco.Cms.Core.Configuration.Models.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 } }; /// @@ -68,34 +70,24 @@ 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 IEnumerable UserDefinedCharCollection { get; set; } + + [Obsolete("Use CharItem in the Umbraco.Cms.Core.Configuration.Models namespace instead. Scheduled for removal in V10.")] public class CharItem : IChar { /// diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 327240ebd8..5ec94381b4 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -11,6 +11,8 @@ namespace Umbraco.Cms.Core.Configuration.Models [UmbracoOptions(Constants.Configuration.ConfigSecurity)] public class SecuritySettings { + internal const bool StaticMemberBypassTwoFactorForExternalLogins = true; + internal const bool StaticUserBypassTwoFactorForExternalLogins = true; internal const bool StaticKeepUserLoggedIn = false; internal const bool StaticHideDisabledUsersInBackOffice = false; internal const bool StaticAllowPasswordReset = true; @@ -66,5 +68,16 @@ namespace Umbraco.Cms.Core.Configuration.Models /// Gets or sets a value for the member password settings. /// public MemberPasswordConfigurationSettings? MemberPassword { get; set; } + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for members. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticMemberBypassTwoFactorForExternalLogins)] + public bool MemberBypassTwoFactorForExternalLogins { get; set; } = StaticMemberBypassTwoFactorForExternalLogins; + + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for users. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] + public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; } } diff --git a/src/Umbraco.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..bdbd13b2a4 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,8 @@ 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"; + public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; } } } 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 cec1cbb4eb..5cb9a7137f 100644 --- a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -7,48 +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..91e6f71415 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -1,9 +1,13 @@ 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; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.Models.Validation; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection { @@ -13,23 +17,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 +58,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 +86,11 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions(); + + builder.Services.Configure(options => options.MergeReplacements(builder.Config)); return builder; } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index df499a8925..ac5137e9ab 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -28,11 +28,14 @@ using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.PublishedCache.Internal; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; @@ -171,7 +174,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); - Services.AddUnique(); + Services.AddSingleton(); // by default, register a noop factory Services.AddUnique(); @@ -179,7 +182,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddSingleton(f => f.GetRequiredService().CreateDictionary()); - Services.AddUnique(); + Services.AddSingleton(); Services.AddUnique(); Services.AddUnique(); @@ -194,14 +197,14 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); // register properties fallback Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); // register published router Services.AddUnique(); @@ -226,13 +229,13 @@ namespace Umbraco.Cms.Core.DependencyInjection // let's use an hybrid accessor that can fall back to a ThreadStatic context. Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); - Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); + Services.AddSingleton(); // register a server registrar, by default it's the db registrar Services.AddUnique(f => @@ -240,7 +243,7 @@ namespace Umbraco.Cms.Core.DependencyInjection GlobalSettings globalSettings = f.GetRequiredService>().Value; var singleServer = globalSettings.DisableElectionForSingleServer; return singleServer - ? (IServerRoleAccessor)new SingleServerRoleAccessor() + ? new SingleServerRoleAccessor() : new ElectedServerRoleAccessor(f.GetRequiredService()); }); @@ -263,6 +266,50 @@ namespace Umbraco.Cms.Core.DependencyInjection // Register telemetry service used to gather data about installed packages Services.AddUnique(); + + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(factory => new ExternalLoginService( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService() + )); + Services.AddUnique(factory => factory.GetRequiredService()); + Services.AddUnique(factory => factory.GetRequiredService()); + Services.AddUnique(factory => new LocalizedTextService( + factory.GetRequiredService>(), + factory.GetRequiredService>())); + + Services.AddUnique(); + + Services.AddSingleton(); + Services.AddSingleton(); + + // Register a noop IHtmlSanitizer to be replaced + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs deleted file mode 100644 index ce4e2a79bb..0000000000 --- a/src/Umbraco.Core/Events/DeleteEventArgs.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Umbraco.Cms.Core.Events -{ - [SupersedeEvent(typeof(SaveEventArgs<>))] - [SupersedeEvent(typeof(PublishEventArgs<>))] - [SupersedeEvent(typeof(MoveEventArgs<>))] - [SupersedeEvent(typeof(CopyEventArgs<>))] - public class DeleteEventArgs : CancellableEnumerableObjectEventArgs, IEquatable>, IDeletingMediaFilesEventArgs - { - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) : base(eventObject, canCancel, eventMessages) - { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) - { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, eventMessages) - { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) - : base(new List { eventObject }, canCancel, eventMessages) - { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, bool canCancel) : base(eventObject, canCancel) - { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - public DeleteEventArgs(IEnumerable eventObject) : base(eventObject) - { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting a single entity instance - /// - /// - public DeleteEventArgs(TEntity eventObject) - : base(new List { eventObject }) - { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, bool canCancel) - : base(new List { eventObject }, canCancel) - { - MediaFilesToDelete = new List(); - } - - /// - /// Returns all entities that were deleted during the operation - /// - public IEnumerable? DeletedEntities - { - get => EventObject; - set => EventObject = value; - } - - /// - /// A list of media files that can be added to during a deleted operation for which Umbraco will ensure are removed - /// - public List MediaFilesToDelete { get; private set; } - - public bool Equals(DeleteEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((DeleteEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397) ^ MediaFilesToDelete.GetHashCode(); - } - } - - public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) - { - return !Equals(left, right); - } - } - - public class DeleteEventArgs : CancellableEventArgs, IEquatable - { - public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) - : base(canCancel, eventMessages) - { - Id = id; - } - - public DeleteEventArgs(int id, bool canCancel) - : base(canCancel) - { - Id = id; - } - - public DeleteEventArgs(int id) - { - Id = id; - } - - /// - /// Gets the Id of the object being deleted. - /// - public int Id { get; private set; } - - public bool Equals(DeleteEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && Id == other.Id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((DeleteEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397) ^ Id; - } - } - - public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) - { - return !Equals(left, right); - } - } -} diff --git a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs index e195e7cfe1..e27c155ec4 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 @@ -96,17 +98,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); } @@ -115,13 +156,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/Events/IDeletingMediaFilesEventArgs.cs b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs deleted file mode 100644 index 9a6a4357e0..0000000000 --- a/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Cms.Core.Events -{ - public interface IDeletingMediaFilesEventArgs - { - List MediaFilesToDelete { get; } - } -} diff --git a/src/Umbraco.Core/Events/IEventDispatcher.cs b/src/Umbraco.Core/Events/IEventDispatcher.cs deleted file mode 100644 index d8d140090d..0000000000 --- a/src/Umbraco.Core/Events/IEventDispatcher.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Cms.Core.Events -{ - /// - /// Dispatches events from within a scope. - /// - /// - /// The name of the event is auto-magically discovered by matching the sender type, args type, and - /// eventHandler type. If the match is not unique, then the name parameter must be used to specify the - /// name in an explicit way. - /// What happens when an event is dispatched depends on the scope settings. It can be anything from - /// "trigger immediately" to "just ignore". Refer to the scope documentation for more details. - /// - public interface IEventDispatcher - { - // not sure about the Dispatch & DispatchCancelable signatures at all for now - // nor about the event name thing, etc - but let's keep it like this - - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? name = null); - - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? name = null) - where TArgs : CancellableEventArgs; - - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null) - where TArgs : CancellableEventArgs; - - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? name = null); - - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? name = null); - - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null); - - /// - /// Notifies the dispatcher that the scope is exiting. - /// - /// A value indicating whether the scope completed. - void ScopeExit(bool completed); - - /// - /// Gets the collected events. - /// - /// The collected events. - IEnumerable GetEvents(EventDefinitionFilter filter); - } -} diff --git a/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs deleted file mode 100644 index d2dafb24f9..0000000000 --- a/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Umbraco.Cms.Core.Events -{ - /// - /// An IEventDispatcher that immediately raise all events. - /// - /// This means that events will be raised during the scope transaction, - /// whatever happens, and the transaction could roll back in the end. - internal class PassThroughEventDispatcher : IEventDispatcher - { - public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) - { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs - { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs - { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } - - public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } - - public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } - - public IEnumerable GetEvents(EventDefinitionFilter filter) - { - return Enumerable.Empty(); - } - - public void ScopeExit(bool completed) - { } - } -} diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcher.cs b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs deleted file mode 100644 index e79cd67cd8..0000000000 --- a/src/Umbraco.Core/Events/QueuingEventDispatcher.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Umbraco.Cms.Core.IO; - -namespace Umbraco.Cms.Core.Events -{ - /// - /// An IEventDispatcher that queues events, and raise them when the scope - /// exits and has been completed. - /// - public class QueuingEventDispatcher : QueuingEventDispatcherBase - { - private readonly MediaFileManager _mediaFileManager; - public QueuingEventDispatcher(MediaFileManager mediaFileManager) - : base(true) - { - _mediaFileManager = mediaFileManager; - } - - protected override void ScopeExitCompleted() - { - // processing only the last instance of each event... - // this is probably far from perfect, because if eg a content is saved in a list - // and then as a single content, the two events will probably not be de-duplicated, - // but it's better than nothing - - foreach (var e in GetEvents(EventDefinitionFilter.LastIn)) - { - e.RaiseEvent(); - - // separating concerns means that this should probably not be here, - // but then where should it be (without making things too complicated)? - var delete = e.Args as IDeletingMediaFilesEventArgs; - if (delete != null && delete.MediaFilesToDelete.Count > 0) - _mediaFileManager.DeleteMediaFiles(delete.MediaFilesToDelete); - } - } - - - - } -} diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs deleted file mode 100644 index 332de83c8e..0000000000 --- a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs +++ /dev/null @@ -1,347 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Collections; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.Events -{ - /// - /// An IEventDispatcher that queues events. - /// - /// - /// Can raise, or ignore, cancelable events, depending on option. - /// Implementations must override ScopeExitCompleted to define what - /// to do with the events when the scope exits and has been completed. - /// If the scope exits without being completed, events are ignored. - /// - public abstract class QueuingEventDispatcherBase : IEventDispatcher - { - //events will be enlisted in the order they are raised - private List? _events; - private readonly bool _raiseCancelable; - - protected QueuingEventDispatcherBase(bool raiseCancelable) - { - _raiseCancelable = raiseCancelable; - } - - private List Events => _events ?? (_events = new List()); - - public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) - { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs - { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs - { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) - { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); - } - - public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); - } - - public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); - } - - public IEnumerable GetEvents(EventDefinitionFilter filter) - { - if (_events == null) - return Enumerable.Empty(); - - IReadOnlyList events; - switch (filter) - { - case EventDefinitionFilter.All: - events = _events; - break; - case EventDefinitionFilter.FirstIn: - var l1 = new OrderedHashSet(); - foreach (var e in _events) - l1.Add(e); - events = l1; - break; - case EventDefinitionFilter.LastIn: - var l2 = new OrderedHashSet(keepOldest: false); - foreach (var e in _events) - l2.Add(e); - events = l2; - break; - default: - throw new ArgumentOutOfRangeException("filter", filter, null); - } - - return FilterSupersededAndUpdateToLatestEntity(events); - } - - private class EventDefinitionInfos - { - public IEventDefinition? EventDefinition { get; set; } - public Type[]? SupersedeTypes { get; set; } - } - - // this is way too convoluted, the supersede attribute is used only on DeleteEventargs to specify - // that it supersedes save, publish, move and copy - BUT - publish event args is also used for - // unpublishing and should NOT be superseded - so really it should not be managed at event args - // level but at event level - // - // what we want is: - // if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should - // not trigger for the entity - and even though, does it make any sense? making a copy of an entity - // should ... trigger? - // - // not going to refactor it all - we probably want to *always* trigger event but tell people that - // due to scopes, they should not expected eg a saved entity to still be around - however, now, - // going to write a ugly condition to deal with U4-10764 - - // iterates over the events (latest first) and filter out any events or entities in event args that are included - // in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want - // to raise the Saved event (well actually we just don't want to include it in the args for that saved event) - internal static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events) - { - // keeps the 'latest' entity and associated event data - var entities = new List>(); - - // collects the event definitions - // collects the arguments in result, that require their entities to be updated - var result = new List(); - var resultArgs = new List(); - - // eagerly fetch superseded arg types for each arg type - var argTypeSuperceeding = events.Select(x => x.Args.GetType()) - .Distinct() - .ToDictionary(x => x, x => x.GetCustomAttributes(false).Select(y => y.SupersededEventArgsType).ToArray()); - - // iterate over all events and filter - // - // process the list in reverse, because events are added in the order they are raised and we want to keep - // the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity - // is Deleted after being Saved, we want to filter out the Saved event - for (var index = events.Count - 1; index >= 0; index--) - { - var def = events[index]; - - var infos = new EventDefinitionInfos - { - EventDefinition = def, - SupersedeTypes = argTypeSuperceeding[def.Args.GetType()] - }; - - var args = def.Args as CancellableObjectEventArgs; - if (args is not null) - { - // not a cancellable event arg, include event definition in result - result.Add(def); - } - else - { - // event object can either be a single object or an enumerable of objects - // try to get as an enumerable, get null if it's not - var eventObjects = args?.EventObject is not null ? TypeHelper.CreateGenericEnumerableFromObject(args.EventObject) : null; - if (eventObjects == null) - { - // single object, cast as an IEntity - // if cannot cast, cannot filter, nothing - just include event definition in result - var eventEntity = args?.EventObject as IEntity; - if (eventEntity == null) - { - result.Add(def); - continue; - } - - // look for this entity in superseding event args - // found = must be removed (ie not added), else track - if (IsSuperceeded(eventEntity, infos, entities) == false) - { - // track - entities.Add(Tuple.Create(eventEntity, infos)); - - // track result arguments - // include event definition in result - resultArgs.Add(args!); - result.Add(def); - } - } - else - { - // enumerable of objects - var toRemove = new List(); - foreach (var eventObject in eventObjects) - { - // extract the event object, cast as an IEntity - // if cannot cast, cannot filter, nothing to do - just leave it in the list & continue - var eventEntity = eventObject as IEntity; - if (eventEntity == null) - continue; - - // look for this entity in superseding event args - // found = must be removed, else track - if (IsSuperceeded(eventEntity, infos, entities)) - toRemove.Add(eventEntity); - else - entities.Add(Tuple.Create(eventEntity, infos)); - } - - // remove superseded entities - foreach (var entity in toRemove) - eventObjects.Remove(entity); - - // if there are still entities in the list, keep the event definition - if (eventObjects.Count > 0) - { - if (toRemove.Count > 0) - { - // re-assign if changed - if (args is not null) - { - args.EventObject = eventObjects; - } - } - - // track result arguments - // include event definition in result - resultArgs.Add(args!); - result.Add(def); - } - } - } - } - - // go over all args in result, and update them with the latest instanceof each entity - UpdateToLatestEntities(entities, resultArgs); - - // reverse, since we processed the list in reverse - result.Reverse(); - - return result; - } - - // edits event args to use the latest instance of each entity - private static void UpdateToLatestEntities(IEnumerable> entities, IEnumerable args) - { - // get the latest entities - // ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates) - var latestEntities = new OrderedHashSet(keepOldest: true); - foreach (var entity in entities.OrderByDescending(entity => entity.Item1.UpdateDate)) - latestEntities.Add(entity.Item1); - - foreach (var arg in args) - { - // event object can either be a single object or an enumerable of objects - // try to get as an enumerable, get null if it's not - var eventObjects = arg.EventObject is not null ? TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject) : null; - if (eventObjects == null) - { - // single object - // look for a more recent entity for that object, and replace if any - // works by "equalling" entities ie the more recent one "equals" this one (though different object) - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject)); - if (foundEntity != null) - arg.EventObject = foundEntity; - } - else - { - // enumerable of objects - // same as above but for each object - var updated = false; - for (var i = 0; i < eventObjects.Count; i++) - { - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i])); - if (foundEntity == null) continue; - eventObjects[i] = foundEntity; - updated = true; - } - - if (updated) - arg.EventObject = eventObjects; - } - } - } - - // determines if a given entity, appearing in a given event definition, should be filtered out, - // considering the entities that have already been visited - an entity is filtered out if it - // appears in another even definition, which supersedes this event definition. - private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List> entities) - { - //var argType = meta.EventArgsType; - var argType = infos.EventDefinition?.Args.GetType(); - - // look for other instances of the same entity, coming from an event args that supersedes other event args, - // ie is marked with the attribute, and is not this event args (cannot supersede itself) - var superceeding = entities - .Where(x => x.Item2.SupersedeTypes?.Length > 0 // has the attribute - && x.Item2.EventDefinition?.Args.GetType() != argType // is not the same - && Equals(x.Item1, entity)) // same entity - .ToArray(); - - // first time we see this entity = not filtered - if (superceeding.Length == 0) - return false; - - // delete event args does NOT supersedes 'unpublished' event - if ((argType?.IsGenericType ?? false) && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition?.EventName == "Unpublished") - return false; - - // found occurrences, need to determine if this event args is superseded - if (argType?.IsGenericType ?? false) - { - // generic, must compare type arguments - var supercededBy = superceeding.FirstOrDefault(x => - x.Item2.SupersedeTypes?.Any(y => - // superseding a generic type which has the same generic type definition - // (but ... no matter the generic type parameters? could be different?) - y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition() - // or superceeding a non-generic type which is ... (but... how is this ever possible? argType *is* generic? - || y.IsGenericTypeDefinition == false && y == argType) ?? false); - return supercededBy != null; - } - else - { - // non-generic, can compare types 1:1 - var supercededBy = superceeding.FirstOrDefault(x => - x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false); - return supercededBy != null; - } - } - - public void ScopeExit(bool completed) - { - if (_events == null) return; - if (completed) - ScopeExitCompleted(); - _events.Clear(); - } - - protected abstract void ScopeExitCompleted(); - } -} diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index 4aebcfec7d..7b077b73a5 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 63bebcca46..fa85fa5420 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 272a80a69e..bb43c2b5d9 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 fce1b36801..523d8ba096 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 @@ -91,5 +92,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 76ce9fe1ba..b7e30c3b19 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 5bdf347ef9..30d1893792 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 c331c04a82..1b8a0a285d 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"; @@ -225,5 +226,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 4636b81e7e..0bb0f8b60f 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -66,14 +66,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. - var isLockedOutProperty = resolved.Where(x => x.Properties is not null).SelectMany(x => x.Properties!).FirstOrDefault(x => x.Alias == Constants.Conventions.Member.IsLockedOut); + // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock) + var isLockedOutProperty = resolved.Where(x => x.Properties is not null).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; @@ -192,20 +189,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 { @@ -213,7 +196,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 { @@ -222,12 +205,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 { @@ -235,7 +216,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 6fb8669af0..8540229f5b 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -68,11 +68,13 @@ 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: - property.SetValue(serializer.Serialize(currentTags.Union(trimmedTags).ToArray()), culture); // json array + var updatedTags = currentTags.Union(trimmedTags).ToArray(); + var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); + property.SetValue(updatedValue, culture); // json array break; } } @@ -81,11 +83,12 @@ 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: - property.SetValue(serializer.Serialize(trimmedTags), culture); // json array + var updatedValue = trimmedTags.Length == 0 ? null : serializer.Serialize(trimmedTags); + property.SetValue(updatedValue, culture); // json array break; } } @@ -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/IUmbracoApplicationLifetimeNotification.cs b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs new file mode 100644 index 0000000000..4b0ea6826a --- /dev/null +++ b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Cms.Core.Notifications +{ + /// + /// Represents an Umbraco application lifetime (starting, started, stopping, stopped) notification. + /// + /// + public interface IUmbracoApplicationLifetimeNotification : INotification + { + /// + /// Gets a value indicating whether Umbraco is restarting (e.g. after an install or upgrade). + /// + /// + /// true if Umbraco is restarting; otherwise, false. + /// + bool IsRestarting { get; } + } +} diff --git a/src/Umbraco.Core/Notifications/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..196af7dfe1 --- /dev/null +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Notifications +{ + /// + /// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured. + /// + /// + public class UmbracoApplicationStartedNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartedNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } +} diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index 4cbf0a55c6..82b87aa3bf 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,23 +1,44 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. +using System; 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 + /// + public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The runtime level - public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) + : this(runtimeLevel, false) + { + // TODO: Remove this constructor in V10 + } /// - /// Gets the runtime level of execution. + /// Initializes a new instance of the class. /// + /// The runtime level + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) + { + RuntimeLevel = runtimeLevel; + IsRestarting = isRestarting; + } + + /// + /// Gets the runtime level. + /// + /// + /// The runtime level. + /// public RuntimeLevel RuntimeLevel { get; } + + /// + public bool IsRestarting { get; } } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs new file mode 100644 index 0000000000..c6dac40a26 --- /dev/null +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Notifications +{ + /// + /// Notification that occurs when Umbraco has completely shutdown. + /// + /// + public class UmbracoApplicationStoppedNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppedNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } +} diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index db86a1e614..062ca954d9 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,4 +1,30 @@ +using System; + 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 : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStoppingNotification() + : this(false) + { + // TODO: Remove this constructor in V10 + } + + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index ca862c23de..7223d8a30a 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; @@ -749,5 +750,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.Infrastructure/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IContentRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs rename to src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IDataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IDataTypeRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IDocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IDocumentBlueprintRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IDocumentRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs index 9b76a8f965..36a07c2fe9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs @@ -1,7 +1,13 @@ -using Umbraco.Cms.Core.Models; +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Persistence.Repositories { public interface IEntityContainerRepository : IReadRepository, IWriteRepository - { } + { + EntityContainer Get(Guid id); + + IEnumerable Get(string name, int level); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs similarity index 64% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index a0bfdd8a53..800981540b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -1,11 +1,9 @@ -using System; +using System; using System.Collections.Generic; -using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Core.Persistence.Repositories { @@ -36,6 +34,7 @@ namespace Umbraco.Cms.Core.Persistence.Repositories UmbracoObjectTypes GetObjectType(int id); UmbracoObjectTypes GetObjectType(Guid key); + int ReserveId(Guid key); IEnumerable GetAllPaths(Guid objectType, params int[] ids); IEnumerable GetAllPaths(Guid objectType, params Guid[] keys); @@ -43,27 +42,6 @@ namespace Umbraco.Cms.Core.Persistence.Repositories bool Exists(int id); bool Exists(Guid key); - /// - /// Gets paged entities for a query and a subset of object types - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// A callback providing the ability to customize the generated SQL used to retrieve entities - /// - /// - /// A collection of mixed entity types which would be of type , , , - /// - /// - IEnumerable GetPagedResultsByQuery( - IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering, Action> sqlCustomization = null); - /// /// Gets paged entities for a query and a specific object type /// @@ -77,7 +55,5 @@ namespace Umbraco.Cms.Core.Persistence.Repositories /// IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); - - } } 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.Infrastructure/Persistence/Repositories/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IMediaRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IMediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IMediaTypeRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IMemberRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IMemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IMemberTypeRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IPublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs similarity index 100% rename from src/Umbraco.Infrastructure/Persistence/Repositories/IPublicAccessRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs 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/Persistence/Repositories/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs index 188c2dc067..3552400b14 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs @@ -106,5 +106,7 @@ namespace Umbraco.Cms.Core.Persistence.Repositories int ClearLoginSessions(int userId); int ClearLoginSessions(TimeSpan timespan); void ClearLoginSession(Guid sessionId); + + IEnumerable GetNextUsers(int id, int count); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs b/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs similarity index 95% rename from src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs rename to src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs index 99c4c3fd1a..321e40cb88 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs @@ -3,12 +3,12 @@ using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Core.Persistence { /// /// String extension methods used specifically to translate into SQL /// - internal static class SqlExpressionExtensions + public static class SqlExpressionExtensions { /// /// Indicates whether two nullable values are equal, substituting a fallback value for nulls. diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/TextColumnType.cs b/src/Umbraco.Core/Persistence/TextColumnType.cs similarity index 56% rename from src/Umbraco.Infrastructure/Persistence/Querying/TextColumnType.cs rename to src/Umbraco.Core/Persistence/TextColumnType.cs index befc1be79f..dc0b8d56bd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/TextColumnType.cs +++ b/src/Umbraco.Core/Persistence/TextColumnType.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Core.Persistence { public enum TextColumnType { diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index 28df816acf..4f9b452f3e 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -149,117 +149,128 @@ namespace Umbraco.Cms.Core.PropertyEditors public virtual bool IsReadOnly => false; /// - /// Used to try to convert the string value to the correct CLR type based on the DatabaseDataType specified for this value editor + /// Used to try to convert the string value to the correct CLR type based on the specified for this value editor. /// - /// - /// - internal Attempt TryConvertValueToCrlType(object? value) + /// The value. + /// + /// The result of the conversion attempt. + /// + /// ValueType was out of range. + internal Attempt TryConvertValueToCrlType(object value) { - // if (value is JValue) - // value = value.ToString(); - - //this is a custom check to avoid any errors, if it's a string and it's empty just make it null + // Ensure empty string values are converted to null if (value is string s && string.IsNullOrWhiteSpace(s)) + { value = null; + } + // Ensure JSON is serialized properly (without indentation or converted to null when empty) + if (value is not null && ValueType.InvariantEquals(ValueTypes.Json)) + { + var jsonValue = _jsonSerializer.Serialize(value); + + if (jsonValue.DetectIsEmptyJson()) + { + value = null; + } + else + { + value = jsonValue; + } + } + + // Convert the string to a known type Type valueType; - //convert the string to a known type switch (ValueTypes.ToStorageType(ValueType)) { case ValueStorageType.Ntext: case ValueStorageType.Nvarchar: valueType = typeof(string); break; - case ValueStorageType.Integer: - //ensure these are nullable so we can return a null if required - //NOTE: This is allowing type of 'long' because I think json.net will deserialize a numerical value as long - // instead of int. Even though our db will not support this (will get truncated), we'll at least parse to this. + case ValueStorageType.Integer: + // Ensure these are nullable so we can return a null if required + // NOTE: This is allowing type of 'long' because I think JSON.NEt will deserialize a numerical value as long instead of int + // Even though our DB will not support this (will get truncated), we'll at least parse to this valueType = typeof(long?); - //if parsing is successful, we need to return as an Int, we're only dealing with long's here because of json.net, we actually - //don't support long values and if we return a long value it will get set as a 'long' on the Property.Value (object) and then - //when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match. + // If parsing is successful, we need to return as an int, we're only dealing with long's here because of JSON.NET, + // we actually don't support long values and if we return a long value, it will get set as a 'long' on the Property.Value (object) and then + // when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match. var result = value.TryConvertTo(valueType); + return result.Success && result.Result != null ? Attempt.Succeed((int)(long)result.Result) : result; case ValueStorageType.Decimal: - //ensure these are nullable so we can return a null if required + // Ensure these are nullable so we can return a null if required valueType = typeof(decimal?); break; case ValueStorageType.Date: - //ensure these are nullable so we can return a null if required + // Ensure these are nullable so we can return a null if required valueType = typeof(DateTime?); break; + default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException("ValueType was out of range."); } + return value.TryConvertTo(valueType); } - /// - /// A method to deserialize the string value that has been saved in the content editor - /// to an object to be stored in the database. - /// - /// - /// - /// The current value that has been persisted to the database for this editor. This value may be useful for - /// how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used. - /// - /// - /// - /// - /// - /// By default this will attempt to automatically convert the string value to the value type supplied by ValueType. - /// - /// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the - /// value to the DB will fail when it tries to validate the value type. - /// + /// + /// A method to deserialize the string value that has been saved in the content editor to an object to be stored in the database. + /// + /// The value returned by the editor. + /// The current value that has been persisted to the database for this editor. This value may be useful for how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used. + /// The value that gets persisted to the database. + /// + /// By default this will attempt to automatically convert the string value to the value type supplied by ValueType. + /// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the + /// value to the DB will fail when it tries to validate the value type. + /// public virtual object? FromEditor(ContentPropertyData editorValue, object currentValue) { - //if it's json but it's empty json, then return null - if (ValueType.InvariantEquals(ValueTypes.Json) && editorValue.Value != null && (editorValue.Value.ToString()?.DetectIsEmptyJson() ?? true)) - { - return null; - } - var result = TryConvertValueToCrlType(editorValue.Value); if (result.Success == false) { StaticApplicationLogging.Logger.LogWarning("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); return null; } + return result.Result; } /// - /// A method used to format the database value to a value that can be used by the editor + /// A method used to format the database value to a value that can be used by the editor. /// - /// - /// - /// - /// + /// The property. + /// The culture. + /// The segment. /// + /// ValueType was out of range. /// - /// The object returned will automatically be serialized into json notation. For most property editors - /// the value returned is probably just a string but in some cases a json structure will be returned. + /// The object returned will automatically be serialized into JSON notation. For most property editors + /// the value returned is probably just a string, but in some cases a JSON structure will be returned. /// public virtual object? ToEditor(IProperty property, string? culture = null, string? segment = null) { - var val = property.GetValue(culture, segment); - if (val == null) return string.Empty; + var value = property.GetValue(culture, segment); + if (value == null) + { + return string.Empty; + } switch (ValueTypes.ToStorageType(ValueType)) { case ValueStorageType.Ntext: case ValueStorageType.Nvarchar: - //if it is a string type, we will attempt to see if it is json stored data, if it is we'll try to convert - //to a real json object so we can pass the true json object directly to angular! - var asString = val.ToString(); - if (asString?.DetectIsJson() ?? false) + // If it is a string type, we will attempt to see if it is JSON stored data, if it is we'll try to convert + // to a real JSON object so we can pass the true JSON object directly to Angular! + var stringValue = value as string ?? value.ToString(); + if (stringValue.DetectIsJson()) { try { @@ -268,32 +279,37 @@ namespace Umbraco.Cms.Core.PropertyEditors } catch { - //swallow this exception, we thought it was json but it really isn't so continue returning a string + // Swallow this exception, we thought it was JSON but it really isn't so continue returning a string } } - return asString; + + return stringValue; + case ValueStorageType.Integer: case ValueStorageType.Decimal: - //Decimals need to be formatted with invariant culture (dots, not commas) - //Anything else falls back to ToString() - var decim = val.TryConvertTo(); - return decim.Success - ? decim.Result.ToString(NumberFormatInfo.InvariantInfo) - : val.ToString(); + // Decimals need to be formatted with invariant culture (dots, not commas) + // Anything else falls back to ToString() + var decimalValue = value.TryConvertTo(); + + return decimalValue.Success + ? decimalValue.Result.ToString(NumberFormatInfo.InvariantInfo) + : value.ToString(); + case ValueStorageType.Date: - var date = val.TryConvertTo(); - if (date.Success == false || date.Result == null) + var dateValue = value.TryConvertTo(); + if (dateValue.Success == false || dateValue.Result == null) { return string.Empty; } - //Dates will be formatted as yyyy-MM-dd HH:mm:ss - return date.Result.Value.ToIsoString(); + + // Dates will be formatted as yyyy-MM-dd HH:mm:ss + return dateValue.Result.Value.ToIsoString(); + default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException("ValueType was out of range."); } } - // TODO: the methods below should be replaced by proper property value convert ToXPath usage! /// diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index 1bad73edc9..c6de78fe42 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -136,8 +136,13 @@ namespace Umbraco.Cms.Core.Routing return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); } - internal UrlInfo? GetUrlFromRoute(string? route, IUmbracoContext umbracoContext, int id, Uri current, - UrlMode mode, string? culture) + internal UrlInfo? GetUrlFromRoute( + string? route, + IUmbracoContext umbracoContext, + int id, + Uri current, + UrlMode mode, + string? culture) { if (string.IsNullOrWhiteSpace(route)) { @@ -151,12 +156,12 @@ namespace Umbraco.Cms.Core.Routing // route is / or / var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); - var domainUri = pos == 0 + DomainAndUri? domainUri = pos == 0 ? null : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture); var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); - if (domainUri is not null || culture == defaultCulture || culture is null) + if (domainUri is not null || culture is null || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) { var url = AssembleUrl(domainUri, path, current, mode).ToString(); return UrlInfo.Url(url, culture); diff --git a/src/Umbraco.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 cd9f5a7e38..e09a8d2205 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.Infrastructure/Scoping/IScope.cs b/src/Umbraco.Core/Scoping/IScope.cs similarity index 79% rename from src/Umbraco.Infrastructure/Scoping/IScope.cs rename to src/Umbraco.Core/Scoping/IScope.cs index 372e329a71..312be1a35f 100644 --- a/src/Umbraco.Infrastructure/Scoping/IScope.cs +++ b/src/Umbraco.Core/Scoping/IScope.cs @@ -1,7 +1,6 @@ using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Core.Scoping { @@ -10,26 +9,6 @@ namespace Umbraco.Cms.Core.Scoping /// public interface IScope : IDisposable, IInstanceIdentifiable { - /// - /// Gets the scope database. - /// - IUmbracoDatabase Database { get; } - - /// - /// Gets the Sql context. - /// - ISqlContext SqlContext { get; } - - /// - /// Gets the scope event messages. - /// - EventMessages Messages { get; } - - /// - /// Gets the scope event dispatcher. - /// - IEventDispatcher Events { get; } - /// /// Gets the scope notification publisher /// @@ -79,6 +58,7 @@ namespace Umbraco.Cms.Core.Scoping void ReadLock(TimeSpan timeout, int lockId); void EagerWriteLock(params int[] lockIds); + void EagerWriteLock(TimeSpan timeout, int lockId); void EagerReadLock(TimeSpan timeout, int lockId); diff --git a/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs similarity index 91% rename from src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs rename to src/Umbraco.Core/Scoping/IScopeProvider.cs index d1f82d2189..c12680763f 100644 --- a/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -1,6 +1,6 @@ using System.Data; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core.Persistence.Querying; #if DEBUG_SCOPES using System.Collections.Generic; @@ -18,7 +18,6 @@ namespace Umbraco.Cms.Core.Scoping /// /// The transaction isolation level. /// The repositories cache mode. - /// An optional events dispatcher. /// An optional notification publisher. /// A value indicating whether to scope the filesystems. /// A value indicating whether this scope should always be registered in the call context. @@ -35,7 +34,6 @@ namespace Umbraco.Cms.Core.Scoping IScope CreateScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher scopedNotificationPublisher = null, bool? scopeFileSystems = null, bool callContext = false, @@ -47,7 +45,6 @@ namespace Umbraco.Cms.Core.Scoping /// A detached scope. /// The transaction isolation level. /// The repositories cache mode. - /// An optional events dispatcher. /// An option notification publisher. /// A value indicating whether to scope the filesystems. /// @@ -58,7 +55,6 @@ namespace Umbraco.Cms.Core.Scoping IScope CreateDetachedScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher scopedNotificationPublisher = null, bool? scopeFileSystems = null); @@ -87,9 +83,9 @@ namespace Umbraco.Cms.Core.Scoping IScopeContext Context { get; } /// - /// Gets the sql context. + /// Creates an instance of /// - ISqlContext SqlContext { get; } + IQuery CreateQuery(); #if DEBUG_SCOPES diff --git a/src/Umbraco.Core/Security/IHtmlSanitizer.cs b/src/Umbraco.Core/Security/IHtmlSanitizer.cs new file mode 100644 index 0000000000..9bcfe405dd --- /dev/null +++ b/src/Umbraco.Core/Security/IHtmlSanitizer.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.Security +{ + public interface IHtmlSanitizer + { + /// + /// Sanitizes HTML + /// + /// HTML to be sanitized + /// Sanitized HTML + string Sanitize(string html); + } +} diff --git a/src/Umbraco.Core/Security/IIdentityUserLogin.cs b/src/Umbraco.Core/Security/IIdentityUserLogin.cs index c2ce78e7f5..c9eb64ceb3 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/Security/NoopHtmlSanitizer.cs b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs new file mode 100644 index 0000000000..2ada23631a --- /dev/null +++ b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Security +{ + public class NoopHtmlSanitizer : IHtmlSanitizer + { + public string Sanitize(string html) + { + return html; + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/ConsentService.cs b/src/Umbraco.Core/Services/ConsentService.cs similarity index 98% rename from src/Umbraco.Infrastructure/Services/Implement/ConsentService.cs rename to src/Umbraco.Core/Services/ConsentService.cs index b00a2579fd..0d7dc77660 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ConsentService.cs +++ b/src/Umbraco.Core/Services/ConsentService.cs @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Implements . diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/ContentService.cs rename to src/Umbraco.Core/Services/ContentService.cs index 020c18b221..5a73ce4c0c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -8,15 +8,15 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Implements the content service. diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeBaseServiceProvider.cs b/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs similarity index 97% rename from src/Umbraco.Infrastructure/Services/Implement/ContentTypeBaseServiceProvider.cs rename to src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs index 619070c297..769f52edf5 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeBaseServiceProvider.cs +++ b/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs @@ -1,7 +1,7 @@ using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class ContentTypeBaseServiceProvider : IContentTypeBaseServiceProvider { diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs similarity index 98% rename from src/Umbraco.Infrastructure/Services/Implement/ContentTypeService.cs rename to src/Umbraco.Core/Services/ContentTypeService.cs index 3d8dcf33bf..11dafd1464 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -8,9 +8,8 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Represents the ContentType Service, which is an easy access to operations involving diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs similarity index 89% rename from src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBase.cs rename to src/Umbraco.Core/Services/ContentTypeServiceBase.cs index d850e0c21a..2aa8f0268f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public abstract class ContentTypeServiceBase : RepositoryService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs rename to src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index f72738b221..d12ed1bddc 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -11,10 +11,9 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; -using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeBaseService where TRepository : IContentTypeRepositoryBase @@ -934,7 +933,7 @@ namespace Umbraco.Cms.Core.Services.Implement { scope.ReadLock(ReadLockIds); // also for containers - return ((EntityContainerRepository) _containerRepository).Get(containerId); + return _containerRepository.Get(containerId); } } @@ -964,7 +963,7 @@ namespace Umbraco.Cms.Core.Services.Implement { scope.ReadLock(ReadLockIds); // also for containers - return ((EntityContainerRepository) _containerRepository).Get(name, level); + return _containerRepository.Get(name, level); } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentVersionService.cs b/src/Umbraco.Core/Services/ContentVersionService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/ContentVersionService.cs rename to src/Umbraco.Core/Services/ContentVersionService.cs index 320410dba6..8b3f18a89f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentVersionService.cs +++ b/src/Umbraco.Core/Services/ContentVersionService.cs @@ -10,7 +10,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; // ReSharper disable once CheckNamespace -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { internal class ContentVersionService : IContentVersionService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/DefaultContentVersionCleanupPolicy.cs b/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs similarity index 97% rename from src/Umbraco.Infrastructure/Services/Implement/DefaultContentVersionCleanupPolicy.cs rename to src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs index 30193396ee..ee4bf6c952 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DefaultContentVersionCleanupPolicy.cs +++ b/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs @@ -6,10 +6,9 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Services; using ContentVersionCleanupPolicySettings = Umbraco.Cms.Core.Models.ContentVersionCleanupPolicySettings; -namespace Umbraco.Cms.Infrastructure.Services.Implement +namespace Umbraco.Cms.Core.Services { public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy { diff --git a/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs similarity index 98% rename from src/Umbraco.Infrastructure/Services/Implement/DomainService.cs rename to src/Umbraco.Core/Services/DomainService.cs index fc5d26e4df..f0ad6d63ee 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class DomainService : RepositoryService, IDomainService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs similarity index 89% rename from src/Umbraco.Infrastructure/Services/Implement/EntityService.cs rename to src/Umbraco.Core/Services/EntityService.cs index 0cbcc8d729..9169c6c96b 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -6,14 +6,12 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; 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.Scoping; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Infrastructure.Persistence.Querying; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class EntityService : RepositoryService, IEntityService { @@ -398,13 +396,7 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var sql = scope.SqlContext.Sql() - .Select(x => x.NodeObjectType) - .From() - .Where(x => x.NodeId == id); - - var guid = scope.Database.ExecuteScalar(sql); - return ObjectTypes.GetUmbracoObjectType(guid); + return _entityRepository.GetObjectType(id); } } @@ -413,13 +405,7 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var sql = scope.SqlContext.Sql() - .Select(x => x.NodeObjectType) - .From() - .Where(x => x.UniqueId == key); - - var guid = scope.Database.ExecuteScalar(sql); - return ObjectTypes.GetUmbracoObjectType(guid); + return _entityRepository.GetObjectType(key); } } @@ -483,36 +469,10 @@ namespace Umbraco.Cms.Core.Services.Implement /// public int ReserveId(Guid key) { - NodeDto node; - using (var scope = ScopeProvider.CreateScope()) + using (ScopeProvider.CreateScope(autoComplete: true)) { - var sql = scope.SqlContext.Sql() - .Select() - .From() - .Where(x => x.UniqueId == key && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation); - - node = scope.Database.SingleOrDefault(sql); - if (node != null) - throw new InvalidOperationException("An identifier has already been reserved for this Udi."); - - node = new NodeDto - { - UniqueId = key, - Text = "RESERVED.ID", - NodeObjectType = Cms.Core.Constants.ObjectTypes.IdReservation, - - CreateDate = DateTime.Now, - UserId = null, - ParentId = -1, - Level = 1, - Path = "-1", - SortOrder = 0, - Trashed = false - }; - scope.Database.Insert(node); - scope.Complete(); + return _entityRepository.ReserveId(key); } - return node.NodeId; } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs rename to src/Umbraco.Core/Services/EntityXmlSerializer.cs index 26060bf988..be4efe2082 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -11,7 +11,7 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Serializes entities to XML diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs similarity index 51% rename from src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs rename to src/Umbraco.Core/Services/ExternalLoginService.cs index 079971be24..291464f16f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/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.Implement +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.Implement } /// - 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.Infrastructure/Services/Implement/FileService.cs b/src/Umbraco.Core/Services/FileService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/FileService.cs rename to src/Umbraco.Core/Services/FileService.cs index 017312e306..7fc97e7c67 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -16,7 +16,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Represents the File Service, which is an easy access to operations involving objects like Scripts, Stylesheets and Templates 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..33a96ad751 --- /dev/null +++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + /// + /// Service handling 2FA logins. + /// + public interface ITwoFactorLoginService : IService + { + /// + /// Deletes all user logins - normally used when a member is deleted. + /// + Task DeleteUserLoginsAsync(Guid userOrMemberKey); + + /// + /// Checks whether 2FA is enabled for the user or member with the specified key. + /// + Task IsTwoFactorEnabledAsync(Guid userOrMemberKey); + + /// + /// Gets the secret for user or member and a specific provider. + /// + Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName); + + /// + /// Gets the setup info for a specific user or member and a specific provider. + /// + /// + /// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by the provider. + /// + Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName); + + /// + /// Gets all registered providers names. + /// + IEnumerable GetAllProviderNames(); + + /// + /// Disables the 2FA provider with the specified provider name for the specified user or member. + /// + Task DisableAsync(Guid userOrMemberKey, string providerName); + + /// + /// Validates the setup of the provider using the secret and code. + /// + bool ValidateTwoFactorSetup(string providerName, string secret, string code); + + /// + /// Saves the 2FA login information. + /// + Task SaveAsync(TwoFactorLogin twoFactorLogin); + + /// + /// Gets all the enabled 2FA providers for the user or member with the specified key. + /// + Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey); + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs b/src/Umbraco.Core/Services/KeyValueService.cs similarity index 98% rename from src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs rename to src/Umbraco.Core/Services/KeyValueService.cs index 7fda83a427..e54a803659 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Core/Services/KeyValueService.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { internal class KeyValueService : IKeyValueService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs rename to src/Umbraco.Core/Services/LocalizationService.cs index b024b8935e..347872fe0b 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Core/Services/LocalizationService.cs @@ -9,7 +9,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Represents the Localization Service, which is an easy access to operations involving and diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/LocalizedTextService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs rename to src/Umbraco.Core/Services/LocalizedTextService.cs index 535dacb3b2..c79d57f389 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/LocalizedTextService.cs @@ -6,7 +6,7 @@ using System.Xml.Linq; using System.Xml.XPath; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// public class LocalizedTextService : ILocalizedTextService diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceFileSources.cs rename to src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs index 5dac893cf4..a24d47fe4b 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care of diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceSupplementaryFileSource.cs b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs similarity index 91% rename from src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceSupplementaryFileSource.cs rename to src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs index 3e847167ce..7fe5e0e48a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceSupplementaryFileSource.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class LocalizedTextServiceSupplementaryFileSource { diff --git a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs b/src/Umbraco.Core/Services/MacroService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/MacroService.cs rename to src/Umbraco.Core/Services/MacroService.cs index a79d9fddce..dd3c2d9af1 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs +++ b/src/Umbraco.Core/Services/MacroService.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Represents the Macro Service, which is an easy access to operations involving diff --git a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/MediaService.cs rename to src/Umbraco.Core/Services/MediaService.cs index 0aa49cde44..def72653bd 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -8,15 +8,15 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Represents the Media Service, which is an easy access to operations involving diff --git a/src/Umbraco.Infrastructure/Services/Implement/MediaTypeService.cs b/src/Umbraco.Core/Services/MediaTypeService.cs similarity index 98% rename from src/Umbraco.Infrastructure/Services/Implement/MediaTypeService.cs rename to src/Umbraco.Core/Services/MediaTypeService.cs index 205e5b176e..49021333cc 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MediaTypeService.cs +++ b/src/Umbraco.Core/Services/MediaTypeService.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class MediaTypeService : ContentTypeServiceBase, IMediaTypeService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs b/src/Umbraco.Core/Services/MemberGroupService.cs similarity index 98% rename from src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs rename to src/Umbraco.Core/Services/MemberGroupService.cs index 9d68415ad5..253f2b433a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs +++ b/src/Umbraco.Core/Services/MemberGroupService.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { internal class MemberGroupService : RepositoryService, IMemberGroupService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/MemberService.cs rename to src/Umbraco.Core/Services/MemberService.cs index 99f21b4fc3..0b8d981143 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -6,13 +6,13 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Represents the MemberService. diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/MemberTypeService.cs rename to src/Umbraco.Core/Services/MemberTypeService.cs index ad0498d0b2..be3831c675 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -9,7 +9,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class MemberTypeService : ContentTypeServiceBase, IMemberTypeService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs rename to src/Umbraco.Core/Services/NotificationService.cs index 447143092a..28e309bef5 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -18,7 +18,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class NotificationService : INotificationService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/PropertyValidationService.cs rename to src/Umbraco.Core/Services/PropertyValidationService.cs index 030b2ae832..91ea968c2a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class PropertyValidationService : IPropertyValidationService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs b/src/Umbraco.Core/Services/PublicAccessService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs rename to src/Umbraco.Core/Services/PublicAccessService.cs index 90184cd258..0d0d82c64c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Core/Services/PublicAccessService.cs @@ -10,7 +10,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { internal class PublicAccessService : RepositoryService, IPublicAccessService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/RedirectUrlService.cs b/src/Umbraco.Core/Services/RedirectUrlService.cs similarity index 98% rename from src/Umbraco.Infrastructure/Services/Implement/RedirectUrlService.cs rename to src/Umbraco.Core/Services/RedirectUrlService.cs index 04cc0c8bc9..0914086289 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/RedirectUrlService.cs +++ b/src/Umbraco.Core/Services/RedirectUrlService.cs @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { internal class RedirectUrlService : RepositoryService, IRedirectUrlService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/RelationService.cs rename to src/Umbraco.Core/Services/RelationService.cs index 7a5d10c222..d4cf07e9d0 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/RelationService.cs @@ -11,7 +11,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { public class RelationService : RepositoryService, IRelationService { diff --git a/src/Umbraco.Infrastructure/Services/Implement/RepositoryService.cs b/src/Umbraco.Core/Services/RepositoryService.cs similarity index 87% rename from src/Umbraco.Infrastructure/Services/Implement/RepositoryService.cs rename to src/Umbraco.Core/Services/RepositoryService.cs index b1e60b73f4..825c30e43d 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/RepositoryService.cs +++ b/src/Umbraco.Core/Services/RepositoryService.cs @@ -1,10 +1,10 @@ -using System; +using System; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Represents a service that works on top of repositories. @@ -12,7 +12,9 @@ namespace Umbraco.Cms.Core.Services.Implement public abstract class RepositoryService : IService { protected IEventMessagesFactory EventMessagesFactory { get; } + protected IScopeProvider ScopeProvider { get; } + protected ILoggerFactory LoggerFactory { get; } protected RepositoryService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) @@ -22,6 +24,6 @@ namespace Umbraco.Cms.Core.Services.Implement LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } - protected IQuery Query() => ScopeProvider.SqlContext.Query(); + protected IQuery Query() => ScopeProvider.CreateQuery(); } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/TagService.cs b/src/Umbraco.Core/Services/TagService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/TagService.cs rename to src/Umbraco.Core/Services/TagService.cs index f701eefe58..a2bbc27cb6 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/TagService.cs +++ b/src/Umbraco.Core/Services/TagService.cs @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Core/Services/UserService.cs similarity index 99% rename from src/Umbraco.Infrastructure/Services/Implement/UserService.cs rename to src/Umbraco.Core/Services/UserService.cs index c64a91a598..47205a3b11 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -10,14 +10,13 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Infrastructure.Persistence.Querying; -using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services { /// /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. @@ -593,7 +592,7 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - return ((UserRepository) _userRepository).GetNextUsers(id, count); + return _userRepository.GetNextUsers(id, count); } } diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs index ebdd86a835..4f2d202155 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 2252aa36f2..6bb7632888 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -17,23 +17,24 @@ - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs index ef7255a0d6..b8fc8d68a5 100644 --- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache diff --git a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs index a04cd69900..6b2be94d45 100644 --- a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -7,6 +7,7 @@ using System.Linq; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache diff --git a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs index 8913de956b..6124d9fb7a 100644 --- a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs +++ b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs @@ -1,10 +1,11 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Core.Cache { diff --git a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index 5b1420be65..f74d602bb8 100644 --- a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -1,8 +1,9 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Core.Cache { diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 6781623fe7..5a4faa6c43 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -50,6 +50,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Runtime; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Extensions; @@ -68,7 +69,6 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection .AddLogging(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => factory.GetRequiredService().CreateDatabase()); builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); builder.NPocoMappers().Add(); builder.PackageMigrationPlans().Add(() => builder.TypeLoader.GetPackageMigrationPlans()); @@ -93,9 +93,11 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Mappers().AddCoreMappers(); // register the scope provider - builder.Services.AddSingleton(); // implements both IScopeProvider and IScopeAccessor + builder.Services.AddSingleton(); // implements IScopeProvider, IScopeAccessor builder.Services.AddSingleton(f => f.GetRequiredService()); builder.Services.AddSingleton(f => f.GetRequiredService()); + + builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs index d061a4372c..da31a8df39 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs @@ -50,7 +50,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection false)); builder.Services.AddUnique, MediaValueSetBuilder>(); builder.Services.AddUnique, MemberValueSetBuilder>(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.AddNotificationHandler(); builder.AddNotificationHandler(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index 6582cfb0c6..ccb515182e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -34,7 +34,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) { // register FileSystems, which manages all filesystems - builder.Services.AddUnique(); + builder.Services.AddSingleton(); // register the scheme for media paths builder.Services.AddUnique(); @@ -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.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index 990b158ef3..e0958bfdb7 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddScoped(); builder.Services.AddTransient(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddTransient(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index d3ebb28f9c..511c09304d 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,5 +1,8 @@ +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; @@ -28,8 +31,11 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddMultipleUnique(); + 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 661ed93292..80d4dd9b3f 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -8,14 +8,17 @@ 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.Services.Implement; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DependencyInjection @@ -28,62 +31,31 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) { // register the service context - builder.Services.AddUnique(); + builder.Services.AddSingleton(); // register the special idk map builder.Services.AddUnique(); - // register the services - 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(); - 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(); 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(); - builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddTransient(SourcesFactory); - builder.Services.AddUnique(factory => new LocalizedTextService( - factory.GetRequiredService>(), - factory.GetRequiredService>())); - - builder.Services.AddUnique(); - - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); - builder.Services.AddUnique(); + 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/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index bf189a374c..2063bbc180 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -79,6 +79,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// public Task StopAsync(CancellationToken cancellationToken) { + _period = Timeout.InfiniteTimeSpan; _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index f7c80452a2..0b631bfaa8 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -2,13 +2,17 @@ // See LICENSE for more details. using System; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration @@ -22,6 +26,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration private readonly IServerRegistrationService _serverRegistrationService; private readonly IHostingEnvironment _hostingEnvironment; private readonly ILogger _logger; + private readonly IServerRoleAccessor _serverRoleAccessor; private GlobalSettings _globalSettings; /// @@ -37,7 +42,8 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration IServerRegistrationService serverRegistrationService, IHostingEnvironment hostingEnvironment, ILogger logger, - IOptionsMonitor globalSettings) + IOptionsMonitor globalSettings, + IServerRoleAccessor serverRoleAccessor) : base(globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) { _runtimeState = runtimeState; @@ -50,6 +56,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration _globalSettings = x; ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls); }); + _serverRoleAccessor = serverRoleAccessor; } public override Task PerformExecuteAsync(object state) @@ -59,6 +66,14 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration return Task.CompletedTask; } + // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense, + // since all it's used for is to allow the ElectedServerRoleAccessor + // to figure out what role a given server has, so we just stop this task. + if (_serverRoleAccessor is not ElectedServerRoleAccessor) + { + return StopAsync(CancellationToken.None); + } + var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); if (serverAddress.IsNullOrWhiteSpace()) { 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/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 28a95b406b..f863aa0d78 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Install @@ -21,6 +22,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install { private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IScopeProvider _scopeProvider; + private readonly IScopeAccessor _scopeAccessor; private readonly IRuntimeState _runtimeState; private readonly IKeyValueService _keyValueService; private readonly ILogger _logger; @@ -38,6 +40,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install /// public DatabaseBuilder( IScopeProvider scopeProvider, + IScopeAccessor scopeAccessor, IUmbracoDatabaseFactory databaseFactory, IRuntimeState runtimeState, ILoggerFactory loggerFactory, @@ -50,6 +53,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) { _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; _databaseFactory = databaseFactory; _runtimeState = runtimeState; _logger = loggerFactory.CreateLogger(); @@ -113,17 +117,17 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install using (var scope = _scopeProvider.CreateScope()) { // look for the super user with default password - var sql = scope.Database.SqlContext.Sql() + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() .SelectCount() .From() .Where(x => x.Id == Constants.Security.SuperUserId && x.Password == "default"); - var result = scope.Database.ExecuteScalar(sql); + var result = _scopeAccessor.AmbientScope.Database.ExecuteScalar(sql); var has = result != 1; if (has == false) { // found only 1 user == the default user with default password // however this always exists on uCloud, also need to check if there are other users too - result = scope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser"); + result = _scopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser"); has = result != 1; } scope.Complete(); @@ -135,7 +139,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install { using (var scope = _scopeProvider.CreateScope(autoComplete: true)) { - return scope.Database.IsUmbracoInstalled(); + return _scopeAccessor.AmbientScope.Database.IsUmbracoInstalled(); } } @@ -336,7 +340,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (_databaseSchemaValidationResult != null) return _databaseSchemaValidationResult; - _databaseSchemaValidationResult = scope.Database.ValidateSchema(); + _databaseSchemaValidationResult = _scopeAccessor.AmbientScope.Database.ValidateSchema(); scope.Complete(); @@ -372,7 +376,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _logger.LogInformation("Database configuration status: Started"); - var database = scope.Database; + var database = _scopeAccessor.AmbientScope.Database; var message = string.Empty; 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/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index 09cddbc20b..358d7d0281 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using Type = System.Type; @@ -12,16 +13,19 @@ namespace Umbraco.Cms.Infrastructure.Migrations public class MigrationPlanExecutor : IMigrationPlanExecutor { private readonly IScopeProvider _scopeProvider; + private readonly IScopeAccessor _scopeAccessor; private readonly ILoggerFactory _loggerFactory; private readonly IMigrationBuilder _migrationBuilder; private readonly ILogger _logger; public MigrationPlanExecutor( IScopeProvider scopeProvider, + IScopeAccessor scopeAccessor, ILoggerFactory loggerFactory, IMigrationBuilder migrationBuilder) { _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; _loggerFactory = loggerFactory; _migrationBuilder = migrationBuilder; _logger = _loggerFactory.CreateLogger(); @@ -61,7 +65,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations // that packages notification handlers may explode because that package isn't fully installed yet. using (scope.Notifications.Suppress()) { - var context = new MigrationContext(plan, scope.Database, _loggerFactory.CreateLogger()); + var context = new MigrationContext(plan, _scopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger()); while (transition != null) { 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/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index defea0ea51..790cefe7e9 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -575,21 +575,22 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var importedFolders = new Dictionary(); var trackEntityContainersInstalled = new List(); - foreach (var documentType in unsortedDocumentTypes) + + foreach (XElement documentType in unsortedDocumentTypes) { - var foldersAttribute = documentType.Attribute("Folders"); - var infoElement = documentType.Element("Info"); + XAttribute foldersAttribute = documentType.Attribute("Folders"); + XElement infoElement = documentType.Element("Info"); if (foldersAttribute != null && infoElement != null - //don't import any folder if this is a child doc type - the parent doc type will need to - //exist which contains it's folders + // don't import any folder if this is a child doc type - the parent doc type will need to + // exist which contains it's folders && ((string)infoElement.Element("Master")).IsNullOrWhiteSpace()) { var alias = documentType.Element("Info").Element("Alias").Value; var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); - var folderKeysAttribute = documentType.Attribute("FolderKeys"); + XAttribute folderKeysAttribute = documentType.Attribute("FolderKeys"); - var folderKeys = Array.Empty(); + Guid[] folderKeys = Array.Empty(); if (folderKeysAttribute != null) { folderKeys = folderKeysAttribute.Value.Split(Constants.CharArrays.ForwardSlash).Select(x=>Guid.Parse(x)).ToArray(); @@ -597,22 +598,22 @@ namespace Umbraco.Cms.Infrastructure.Packaging var rootFolder = WebUtility.UrlDecode(folders[0]); - EntityContainer current; + EntityContainer current = null; Guid? rootFolderKey = null; if (folderKeys.Length == folders.Length && folderKeys.Length > 0) { rootFolderKey = folderKeys[0]; current = _contentTypeService.GetContainer(rootFolderKey.Value); } - else - { - //level 1 = root level folders, there can only be one with the same name - current = _contentTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); - } + + // The folder might already exist, but with a different key, so check if it exists, even if there is a key. + // Level 1 = root level folders, there can only be one with the same name + current ??= _contentTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); if (current == null) { - var tryCreateFolder = _contentTypeService.CreateContainer(-1, rootFolderKey ?? Guid.NewGuid(), rootFolder); + Attempt> tryCreateFolder = _contentTypeService.CreateContainer(-1, rootFolderKey ?? Guid.NewGuid(), rootFolder); + if (tryCreateFolder == false) { _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", rootFolder); @@ -644,7 +645,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging private EntityContainer CreateContentTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) { var children = _entityService.GetChildren(current.Id).ToArray(); - var found = children.Any(x => x.Name.InvariantEquals(folderName) ||x.Key.Equals(folderKey)); + var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); if (found) { var containerId = children.Single(x => x.Name.InvariantEquals(folderName)).Id; 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/IEntityRepositoryExtended.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs new file mode 100644 index 0000000000..7e1b2ad0df --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using NPoco; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories +{ + public interface IEntityRepositoryExtended : IEntityRepository + { + /// + /// Gets paged entities for a query and a subset of object types + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// A callback providing the ability to customize the generated SQL used to retrieve entities + /// + /// + /// A collection of mixed entity types which would be of type , , , + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering, Action> sqlCustomization = null); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs index e3685dd32c..9cadc82e49 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs index d528f69dce..644dc4bc26 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs @@ -8,9 +8,9 @@ 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.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs index d0e025e06e..3ce9aad530 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs @@ -4,9 +4,9 @@ using System.Linq; using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement @@ -21,7 +21,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; /// - private IScope AmbientScope => _scopeAccessor.AmbientScope; + private IDatabaseScope AmbientScope => _scopeAccessor.AmbientScope; /// public int CountAll() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs index 87a112fe08..194369285b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs @@ -6,10 +6,10 @@ 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.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index fd4d1c33b9..0b4af6bdee 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -15,12 +15,12 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 04ca55b499..66ab87b743 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using Enumerable = System.Linq.Enumerable; @@ -42,7 +43,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement _shortStringHelper = shortStringHelper; } - private IScope AmbientScope => _scopeAccessor.AmbientScope; + private IDatabaseScope AmbientScope => _scopeAccessor.AmbientScope; private IUmbracoDatabase Database => AmbientScope.Database; private ISqlContext SqlContext => AmbientScope.SqlContext; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index 9b0fe45c79..56fd356011 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -8,11 +8,11 @@ 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.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 38e8a99d08..5ffdf4bf10 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Globalization; @@ -12,12 +12,12 @@ using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement 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..462ae0a64e --- /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( + IUmbracoDatabaseFactory umbracoDatabaseFactory, + 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 = umbracoDatabaseFactory.CreateDatabase(); + _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/DataTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs index 6128b2e9b2..f8fc9e14be 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs index f70048c4f9..954d3e0d0f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -13,12 +13,12 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index bc9892b1ee..98b1820204 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -9,10 +9,10 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; 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.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index 79663c292e..f97aec0917 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -4,9 +4,9 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 41645dba27..0d77f3d53b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -21,6 +21,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs index c6c0df848c..f37886fee2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs index 21bd883c68..52b73bed26 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs index d7fd2e9c7c..7633264ed6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs @@ -11,6 +11,7 @@ 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.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs index b30c5ae1a4..37cbf979e2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs @@ -8,8 +8,8 @@ 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.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index f1b9c77d0a..965c85b6e6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -8,11 +8,11 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// Limited to objects that have a corresponding node (in umbracoNode table). /// Returns objects, i.e. lightweight representation of entities. /// - internal class EntityRepository : RepositoryBase, IEntityRepository + internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended { public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) : base(scopeAccessor, appCaches) @@ -251,6 +251,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); } + public int ReserveId(Guid key) + { + NodeDto node; + + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.UniqueId == key && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation); + + node = Database.SingleOrDefault(sql); + if (node != null) + throw new InvalidOperationException("An identifier has already been reserved for this Udi."); + + node = new NodeDto + { + UniqueId = key, + Text = "RESERVED.ID", + NodeObjectType = Cms.Core.Constants.ObjectTypes.IdReservation, + + CreateDate = DateTime.Now, + UserId = null, + ParentId = -1, + Level = 1, + Path = "-1", + SortOrder = 0, + Trashed = false + }; + Database.Insert(node); + + return node.NodeId; + } + public bool Exists(Guid key) { var sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs index 79e6f732a2..3518803490 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 814bfefc88..955cbf5d5d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -6,34 +6,45 @@ 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.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; 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/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index 288f480ed1..653e916be6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -7,9 +7,9 @@ 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.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs index d0aee52406..fc40343692 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs @@ -10,10 +10,10 @@ using Umbraco.Cms.Core.Configuration.Models; 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.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs index 556aba8e22..3482da377d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -8,8 +8,8 @@ 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.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs index 535895e8ed..c0ba8f3ac4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs @@ -9,11 +9,11 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index 29960a4044..a431fb6896 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -12,12 +12,12 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs index 7710f3efb1..069b49de2f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs index 26d24cbac0..0a07fb2260 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -8,10 +8,10 @@ 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.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs index 970e498b26..89858951d7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -9,10 +9,10 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; 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.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index a1b15f407d..4a3ed0aba6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; @@ -21,6 +22,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs index ca3edea81c..27e51551e2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -8,11 +8,11 @@ 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.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs index 2d68c95fe2..c35052b9d1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Models; @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement @@ -20,7 +21,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement _scopeAccessor = scopeAccessor; } - private IScope AmbientScope => _scopeAccessor.AmbientScope; + private IDatabaseScope AmbientScope => _scopeAccessor.AmbientScope; public IEnumerable GetUsersNotifications(IEnumerable userIds, string action, IEnumerable nodeIds, Guid objectType) { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs index 2b38d55212..afcefdee2c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs @@ -9,8 +9,8 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs index 1c4dcdb675..dbddb56b68 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs @@ -8,10 +8,10 @@ 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.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index 6ab29aa47e..5a15cc5b3d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -9,8 +9,8 @@ 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.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index 749fc9d77b..718ea04f63 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -9,12 +9,12 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; @@ -26,9 +26,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement internal class RelationRepository : EntityRepositoryBase, IRelationRepository { private readonly IRelationTypeRepository _relationTypeRepository; - private readonly IEntityRepository _entityRepository; + private readonly IEntityRepositoryExtended _entityRepository; - public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepository entityRepository) + public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepositoryExtended entityRepository) : base(scopeAccessor, AppCaches.NoCache, logger) { _relationTypeRepository = relationTypeRepository; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs index 151d08a2ff..f50e307cfa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs @@ -9,10 +9,10 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; 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.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs index 6e550f9362..4f82603710 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs @@ -3,8 +3,8 @@ using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { @@ -35,11 +35,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// /// Gets the AmbientScope /// - protected IScope AmbientScope + protected IDatabaseScope AmbientScope { get { - IScope scope = ScopeAccessor.AmbientScope; + IDatabaseScope scope = ScopeAccessor.AmbientScope; if (scope == null) { throw new InvalidOperationException("Cannot run a repository without an ambient scope."); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs index 98aef6fa6d..4d1ef3fa88 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs index 6053d35085..2b7257ff8e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs @@ -6,8 +6,8 @@ using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs index 9c0f0bfe66..900ff2934b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs @@ -9,10 +9,10 @@ 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.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs index 6d2c02484a..f318d4d31e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs @@ -12,11 +12,11 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement 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..4b3ccb4d6a --- /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.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; +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/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index d6be3cf730..b27f42858a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -10,11 +10,11 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 90ab0fa88c..b4724b8885 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -13,13 +13,13 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement @@ -911,7 +911,7 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 return sql; } - internal IEnumerable GetNextUsers(int id, int count) + public IEnumerable GetNextUsers(int id, int count) { var idsQuery = SqlContext.Sql() .Select(x => x.Id) 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/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 75d348df1a..b9890d85d6 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Text.RegularExpressions; using NPoco; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Querying; diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs index 0093ee14ce..f045f379e4 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Data; using System.Linq; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Querying; namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax 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/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index db6adeca77..5dbf97021d 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.RegularExpressions; using NPoco; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Querying; diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs index 86ffc1b128..ae2a17dc7c 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; @@ -32,7 +33,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence var whereParam = sqlSyntax.GetStringColumnWildcardComparison( sqlSyntax.GetQuotedColumnName("key"), 0, - Querying.TextColumnType.NVarchar); + TextColumnType.NVarchar); var sql = database.SqlContext.Sql() .Select() 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 59b3585144..5082cbedcb 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 7152894cb4..9336442fe6 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -1,12 +1,10 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; @@ -139,7 +137,7 @@ namespace Umbraco.Cms.Core.PropertyEditors } // Convert back to raw JSON for persisting - return JsonConvert.SerializeObject(grid); + return JsonConvert.SerializeObject(grid, Formatting.None); } /// @@ -153,7 +151,8 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object ToEditor(IProperty property, string? culture = null, string? segment = null) { var val = property.GetValue(culture, segment)?.ToString(); - if (val.IsNullOrWhiteSpace()) return string.Empty; + if (val.IsNullOrWhiteSpace()) + return string.Empty; var grid = DeserializeGridValue(val, out var rtes, out _); @@ -199,7 +198,7 @@ namespace Umbraco.Cms.Core.PropertyEditors _richTextPropertyValueEditor.GetReferences(x.Value))) yield return umbracoEntityReference; - foreach (var umbracoEntityReference in mediaValues.Where(x=>x.Value.HasValues) + foreach (var umbracoEntityReference in mediaValues.Where(x => x.Value.HasValues) .SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index 8afdb42419..975d0f9487 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -210,10 +210,11 @@ namespace Umbraco.Cms.Core.PropertyEditors { continue; } + var sourcePath = _mediaFileManager.FileSystem.GetRelativePath(src); var copyPath = _mediaFileManager.CopyFile(notification.Copy, property.PropertyType, sourcePath); jo["src"] = _mediaFileManager.FileSystem.GetUrl(copyPath); - 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 +275,9 @@ 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 - { - src = svalue, - crops = config == null ? Array.Empty() : config.Crops - }; - property.SetValue(JsonConvert.SerializeObject(json), pvalue.Culture, pvalue.Segment); + property.SetValue(JsonConvert.SerializeObject(new { src = svalue }, Formatting.None), pvalue.Culture, pvalue.Segment); } else { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs index 587d5e146a..0ece3a8683 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -87,31 +87,42 @@ namespace Umbraco.Cms.Core.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { - // get the current path + // Get the current path var currentPath = string.Empty; try { var svalue = currentValue as string; var currentJson = string.IsNullOrWhiteSpace(svalue) ? null : JObject.Parse(svalue); - if (currentJson != null && currentJson["src"] != null) - currentPath = currentJson["src"].Value(); + if (currentJson != null && currentJson.TryGetValue("src", out var src)) + { + currentPath = src.Value(); + } } catch (Exception ex) { - // for some reason the value is invalid so continue as if there was no value there + // For some reason the value is invalid so continue as if there was no value there _logger.LogWarning(ex, "Could not parse current db value to a JObject."); } + if (string.IsNullOrWhiteSpace(currentPath) == false) currentPath = _mediaFileManager.FileSystem.GetRelativePath(currentPath); - // get the new json and path - JObject editorJson = null; + // Get the new JSON and file path var editorFile = string.Empty; - if (editorValue.Value != null) + if (editorValue.Value is JObject editorJson) { - editorJson = editorValue.Value as JObject; - if (editorJson != null && editorJson["src"] != null) + // Populate current file + if (editorJson["src"] != null) + { editorFile = editorJson["src"].Value(); + } + + // Clean up redundant/default data + ImageCropperValue.Prune(editorJson); + } + else + { + editorJson = null; } // ensure we have the required guids @@ -139,7 +150,7 @@ namespace Umbraco.Cms.Core.PropertyEditors return null; // clear } - return editorJson?.ToString(); // unchanged + return editorJson?.ToString(Formatting.None); // unchanged } // process the file @@ -160,7 +171,7 @@ namespace Umbraco.Cms.Core.PropertyEditors // update json and return if (editorJson == null) return null; editorJson["src"] = filepath == null ? string.Empty : _mediaFileManager.FileSystem.GetUrl(filepath); - return editorJson.ToString(); + return editorJson.ToString(Formatting.None); } private string ProcessFile(ContentPropertyFile file, Guid cuid, Guid puid) @@ -187,7 +198,6 @@ namespace Umbraco.Cms.Core.PropertyEditors return filepath; } - public override string ConvertDbToString(IPropertyType propertyType, object value) { if (value == null || string.IsNullOrEmpty(value.ToString())) @@ -206,6 +216,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 58a2397c5c..6a9cd39e3d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; @@ -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); } /// @@ -124,7 +157,6 @@ namespace Umbraco.Cms.Core.PropertyEditors } } - /// /// Model/DTO that represents the JSON that the MediaPicker3 stores. /// @@ -142,6 +174,48 @@ 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. + /// + /// 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 090a8ecaec..6b9bb73b14 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -57,7 +57,7 @@ namespace Umbraco.Cms.Core.PropertyEditors try { - var links = JsonConvert.DeserializeObject>(value); + var links = JsonConvert.DeserializeObject>(value); var documentLinks = links.FindAll(link => link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Document); var mediaLinks = links.FindAll(link => link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Media); @@ -141,6 +141,7 @@ namespace Umbraco.Cms.Core.PropertyEditors private static readonly JsonSerializerSettings LinkDisplayJsonSerializerSettings = new JsonSerializerSettings { + Formatting = Formatting.None, NullValueHandling = NullValueHandling.Ignore }; @@ -150,22 +151,28 @@ 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) - select new MultiUrlPickerValueEditor.LinkDto + from link in links + select new LinkDto { Name = link.Name, QueryString = link.QueryString, 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 574a33f9f1..71a7a89be5 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -1,14 +1,12 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Exceptions; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -80,7 +78,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 f22f0e54aa..f61c5c78d0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -59,14 +59,18 @@ 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(); + if (values.Length == 0) + { + return null; + } - return JsonConvert.SerializeObject(values); + return JsonConvert.SerializeObject(values, Formatting.None); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 06c9db7c5b..0cbd4e1155 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -105,7 +105,9 @@ namespace Umbraco.Cms.Core.PropertyEditors var rows = _nestedContentValues.GetPropertyValues(propertyValue); if (rows.Count == 0) - return string.Empty; + { + return null; + } foreach (var row in rows.ToList()) { @@ -136,13 +138,11 @@ namespace Umbraco.Cms.Core.PropertyEditors } } - return JsonConvert.SerializeObject(rows).ToXmlString(); + return JsonConvert.SerializeObject(rows, Formatting.None).ToXmlString(); } #endregion - - #region Convert database // editor // note: there is NO variant support here @@ -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 e00ab611cc..857857bbc2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -81,6 +81,7 @@ namespace Umbraco.Cms.Core.PropertyEditors private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IHtmlSanitizer _htmlSanitizer; public RichTextPropertyValueEditor( DataEditorAttribute attribute, @@ -92,7 +93,8 @@ namespace Umbraco.Cms.Core.PropertyEditors RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator, IJsonSerializer jsonSerializer, - IIOHelper ioHelper) + IIOHelper ioHelper, + IHtmlSanitizer htmlSanitizer) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -100,6 +102,7 @@ namespace Umbraco.Cms.Core.PropertyEditors _localLinkParser = localLinkParser; _pastedImages = pastedImages; _imageUrlGenerator = imageUrlGenerator; + _htmlSanitizer = htmlSanitizer; } /// @@ -145,7 +148,9 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object FromEditor(ContentPropertyData editorValue, object currentValue) { if (editorValue.Value == null) + { return null; + } var userId = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser?.Id ?? Constants.Security.SuperUserId; @@ -156,8 +161,9 @@ namespace Umbraco.Cms.Core.PropertyEditors var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); + var sanitized = _htmlSanitizer.Sanitize(parsed); - return parsed; + return sanitized.NullOrWhiteSpaceAsNull(); } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index 4683851936..30911b0866 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -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..af9e820d66 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; @@ -20,14 +21,14 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// [JsonConverter(typeof(NoTypeConverterJsonConverter))] [TypeConverter(typeof(ImageCropperValueTypeConverter))] - [DataContract(Name="imageCropDataSet")] + [DataContract(Name = "imageCropDataSet")] public class ImageCropperValue : IHtmlEncodedString, IEquatable { /// /// Gets or sets the value source image. /// - [DataMember(Name="src")] - public string Src { get; set;} + [DataMember(Name = "src")] + public string Src { get; set; } /// /// Gets or sets the value focal point. @@ -43,9 +44,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// public override string ToString() - { - return Crops != null ? (Crops.Any() ? JsonConvert.SerializeObject(this) : Src) : string.Empty; - } + => HasCrops() || HasFocalPoint() ? JsonConvert.SerializeObject(this, Formatting.None) : Src; /// public string ToHtmlString() => Src; @@ -122,13 +121,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 +172,49 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters }; } + /// + /// Removes redundant crop data/default focal point. + /// + /// The image cropper 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 /// @@ -200,8 +248,8 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters // properties are, practically, readonly // ReSharper disable NonReadonlyMemberInGetHashCode var hashCode = Src?.GetHashCode() ?? 0; - hashCode = (hashCode*397) ^ (FocalPoint?.GetHashCode() ?? 0); - hashCode = (hashCode*397) ^ (Crops?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (FocalPoint?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Crops?.GetHashCode() ?? 0); return hashCode; // ReSharper restore NonReadonlyMemberInGetHashCode } @@ -246,7 +294,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { // properties are, practically, readonly // ReSharper disable NonReadonlyMemberInGetHashCode - return (Left.GetHashCode()*397) ^ Top.GetHashCode(); + return (Left.GetHashCode() * 397) ^ Top.GetHashCode(); // ReSharper restore NonReadonlyMemberInGetHashCode } } @@ -300,9 +348,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters // properties are, practically, readonly // ReSharper disable NonReadonlyMemberInGetHashCode var hashCode = Alias?.GetHashCode() ?? 0; - hashCode = (hashCode*397) ^ Width; - hashCode = (hashCode*397) ^ Height; - hashCode = (hashCode*397) ^ (Coordinates?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ Width; + hashCode = (hashCode * 397) ^ Height; + hashCode = (hashCode * 397) ^ (Coordinates?.GetHashCode() ?? 0); return hashCode; // ReSharper restore NonReadonlyMemberInGetHashCode } @@ -357,9 +405,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters // properties are, practically, readonly // ReSharper disable NonReadonlyMemberInGetHashCode var hashCode = X1.GetHashCode(); - hashCode = (hashCode*397) ^ Y1.GetHashCode(); - hashCode = (hashCode*397) ^ X2.GetHashCode(); - hashCode = (hashCode*397) ^ Y2.GetHashCode(); + hashCode = (hashCode * 397) ^ Y1.GetHashCode(); + hashCode = (hashCode * 397) ^ X2.GetHashCode(); + hashCode = (hashCode * 397) ^ Y2.GetHashCode(); return hashCode; // ReSharper restore NonReadonlyMemberInGetHashCode } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 175bceb9e0..851d67e713 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. @@ -100,49 +134,48 @@ namespace Umbraco.Cms.Infrastructure.Runtime public IRuntimeState State { get; } /// - public async Task RestartAsync() - { - await StopAsync(_cancellationToken); - await StartAsync(_cancellationToken); - } + public async Task StartAsync(CancellationToken cancellationToken) => await StartAsync(cancellationToken, false); /// - public async Task StartAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) => await StopAsync(cancellationToken, false); + + /// + public async Task RestartAsync() { + await StopAsync(_cancellationToken, true); + await _eventAggregator.PublishAsync(new UmbracoApplicationStoppedNotification(true), _cancellationToken); + await StartAsync(_cancellationToken, true); + await _eventAggregator.PublishAsync(new UmbracoApplicationStartedNotification(true), _cancellationToken); + } + + private async Task StartAsync(CancellationToken cancellationToken, bool isRestarting) + { + // Store token, so we can re-use this during restart _cancellationToken = cancellationToken; - StaticApplicationLogging.Initialize(_loggerFactory); - StaticServiceProvider.Instance = _serviceProvider; - AppDomain.CurrentDomain.UnhandledException += (_, args) => + if (isRestarting == false) { - var exception = (Exception)args.ExceptionObject; - var isTerminating = args.IsTerminating; // always true? + StaticApplicationLogging.Initialize(_loggerFactory); + StaticServiceProvider.Instance = _serviceProvider; - var msg = "Unhandled exception in AppDomain"; + AppDomain.CurrentDomain.UnhandledException += (_, args) + => _logger.LogError(args.ExceptionObject as Exception, $"Unhandled exception in AppDomain{(args.IsTerminating ? " (terminating)" : null)}."); + } - if (isTerminating) - { - msg += " (terminating)"; - } - - msg += "."; - - _logger.LogError(exception, msg); - }; - - // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate + // Acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); // TODO (V10): Remove this obsoleted notification publish. await _eventAggregator.PublishAsync(new UmbracoApplicationMainDomAcquiredNotification(), cancellationToken); - // notify for unattended install - await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification()); + // Notify for unattended install + await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification(), cancellationToken); DetermineRuntimeLevel(); if (!State.UmbracoCanBoot()) { - return; // The exception will be rethrown by BootFailedMiddelware + // We cannot continue here, the exception will be rethrown by BootFailedMiddelware + return; } IApplicationShutdownRegistry hostingEnvironmentLifetime = _applicationShutdownRegistry; @@ -151,9 +184,9 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}"); } - // if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade + // If level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade var unattendedUpgradeNotification = new RuntimeUnattendedUpgradeNotification(); - await _eventAggregator.PublishAsync(unattendedUpgradeNotification); + await _eventAggregator.PublishAsync(unattendedUpgradeNotification, cancellationToken); switch (unattendedUpgradeNotification.UnattendedUpgradeResult) { case RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors: @@ -161,54 +194,60 @@ namespace Umbraco.Cms.Infrastructure.Runtime { throw new InvalidOperationException($"Unattended upgrade result was {RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors} but no {nameof(BootFailedException)} was registered"); } - // we cannot continue here, the exception will be rethrown by BootFailedMiddelware + + // We cannot continue here, the exception will be rethrown by BootFailedMiddelware return; case RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete: case RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete: - // upgrade is done, set reason to Run + // Upgrade is done, set reason to Run DetermineRuntimeLevel(); break; case RuntimeUnattendedUpgradeNotification.UpgradeResult.NotRequired: break; } - // TODO (V10): Remove this obsoleted notification publish. + // TODO (V10): Remove this obsoleted notification publish await _eventAggregator.PublishAsync(new UmbracoApplicationComponentsInstallingNotification(State.Level), cancellationToken); - // create & initialize the components + // Initialize the components _components.Initialize(); - await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level), cancellationToken); + await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level, isRestarting), cancellationToken); + + if (isRestarting == false) + { + // Add application started and stopped notifications last (to ensure they're always published after starting) + _hostApplicationLifetime.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification(false))); + _hostApplicationLifetime.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification(false))); + } } - public async Task StopAsync(CancellationToken cancellationToken) + private async Task StopAsync(CancellationToken cancellationToken, bool isRestarting) { _components.Terminate(); - await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(), cancellationToken); - StaticApplicationLogging.Initialize(null); + await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(isRestarting), cancellationToken); } private void AcquireMainDom() { - using (DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) + using DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired."); + + try { - try - { - _mainDom.Acquire(_applicationShutdownRegistry); - } - catch - { - timer?.Fail(); - throw; - } + _mainDom.Acquire(_applicationShutdownRegistry); + } + catch + { + timer?.Fail(); + throw; } } private void DetermineRuntimeLevel() { - if (State.BootFailedException != null) + if (State.BootFailedException is not null) { - // there's already been an exception so cannot boot and no need to check + // There's already been an exception, so cannot boot and no need to check return; } @@ -231,7 +270,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime State.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException); timer?.Fail(); _logger.LogError(ex, "Boot Failed"); - // We do not throw the exception. It will be rethrown by BootFailedMiddleware + + // We do not throw the exception, it will be rethrown by BootFailedMiddleware } } } diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 8eab08bfee..c81041849a 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 @@ -41,16 +44,14 @@ namespace Umbraco.Cms.Infrastructure.Runtime { } - /// - /// Initializes a new instance of the class. - /// public RuntimeState( IOptions globalSettings, IOptions unattendedSettings, IUmbracoVersion umbracoVersion, IUmbracoDatabaseFactory databaseFactory, ILogger logger, - PendingPackageMigrations packageMigrationState) + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService) { _globalSettings = globalSettings; _unattendedSettings = unattendedSettings; @@ -58,8 +59,30 @@ namespace Umbraco.Cms.Infrastructure.Runtime _databaseFactory = databaseFactory; _logger = logger; _packageMigrationState = packageMigrationState; + _conflictingRouteService = conflictingRouteService; } + /// + /// Initializes a new instance of the class. + /// + [Obsolete("use ctor with all params")] + public RuntimeState( + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + StaticServiceProvider.Instance.GetRequiredService()) + { + } /// public Version Version => _umbracoVersion.Version; @@ -101,6 +124,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/Scoping/IDatabaseScope.cs b/src/Umbraco.Infrastructure/Scoping/IDatabaseScope.cs new file mode 100644 index 0000000000..aac5f48b4b --- /dev/null +++ b/src/Umbraco.Infrastructure/Scoping/IDatabaseScope.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Infrastructure.Scoping +{ + public interface IDatabaseScope : IScope + { + /// + /// Gets the scope database. + /// + IUmbracoDatabase Database { get; } + + /// + /// Gets the Sql context. + /// + ISqlContext SqlContext { get; } + } +} diff --git a/src/Umbraco.Infrastructure/Scoping/IScopeAccessor.cs b/src/Umbraco.Infrastructure/Scoping/IScopeAccessor.cs index a699a8bc3e..234ee0232b 100644 --- a/src/Umbraco.Infrastructure/Scoping/IScopeAccessor.cs +++ b/src/Umbraco.Infrastructure/Scoping/IScopeAccessor.cs @@ -1,17 +1,11 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -namespace Umbraco.Cms.Core.Scoping +namespace Umbraco.Cms.Infrastructure.Scoping { - /// - /// Provides access to the ambient scope. - /// public interface IScopeAccessor { /// /// Gets the ambient scope. /// /// Returns null if there is no ambient scope. - IScope AmbientScope { get; } + IDatabaseScope AmbientScope { get; } } } diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index 2383e8eb92..1c1bd636b9 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Core.Collections; using Umbraco.Extensions; @@ -19,7 +20,7 @@ namespace Umbraco.Cms.Core.Scoping /// Implements . /// /// Not thread-safe obviously. - internal class Scope : IScope + internal class Scope : IDatabaseScope { private readonly bool _autoComplete; private readonly CoreDebugSettings _coreDebugSettings; @@ -29,7 +30,6 @@ namespace Umbraco.Cms.Core.Scoping private readonly IsolationLevel _isolationLevel; private readonly object _lockQueueLocker = new(); private readonly ILogger _logger; - private readonly MediaFileManager _mediaFileManager; private readonly RepositoryCacheMode _repositoryCacheMode; private readonly bool? _scopeFileSystem; @@ -39,11 +39,9 @@ namespace Umbraco.Cms.Core.Scoping private IUmbracoDatabase _database; private bool _disposed; - private IEventDispatcher _eventDispatcher; private ICompletable _fscope; private IsolatedCaches _isolatedCaches; - private EventMessages _messages; private IScopedNotificationPublisher _notificationPublisher; private StackQueue<(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId)> _queuedLocks; @@ -59,7 +57,6 @@ namespace Umbraco.Cms.Core.Scoping private Scope( ScopeProvider scopeProvider, CoreDebugSettings coreDebugSettings, - MediaFileManager mediaFileManager, IEventAggregator eventAggregator, ILogger logger, FileSystems fileSystems, @@ -68,7 +65,6 @@ namespace Umbraco.Cms.Core.Scoping bool detachable, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher notificationPublisher = null, bool? scopeFileSystems = null, bool callContext = false, @@ -76,14 +72,12 @@ namespace Umbraco.Cms.Core.Scoping { _scopeProvider = scopeProvider; _coreDebugSettings = coreDebugSettings; - _mediaFileManager = mediaFileManager; _eventAggregator = eventAggregator; _logger = logger; Context = scopeContext; _isolationLevel = isolationLevel; _repositoryCacheMode = repositoryCacheMode; - _eventDispatcher = eventDispatcher; _notificationPublisher = notificationPublisher; _scopeFileSystem = scopeFileSystems; _callContext = callContext; @@ -141,12 +135,6 @@ namespace Umbraco.Cms.Core.Scoping nameof(repositoryCacheMode)); } - // cannot specify a dispatcher! - if (_eventDispatcher != null) - { - throw new ArgumentException("Value cannot be specified on nested scope.", nameof(eventDispatcher)); - } - // Only the outermost scope can specify the notification publisher if (_notificationPublisher != null) { @@ -179,7 +167,6 @@ namespace Umbraco.Cms.Core.Scoping public Scope( ScopeProvider scopeProvider, CoreDebugSettings coreDebugSettings, - MediaFileManager mediaFileManager, IEventAggregator eventAggregator, ILogger logger, FileSystems fileSystems, @@ -187,13 +174,12 @@ namespace Umbraco.Cms.Core.Scoping IScopeContext scopeContext, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher scopedNotificationPublisher = null, bool? scopeFileSystems = null, bool callContext = false, bool autoComplete = false) - : this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, null, - scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, + : this(scopeProvider, coreDebugSettings, eventAggregator, logger, fileSystems, null, + scopeContext, detachable, isolationLevel, repositoryCacheMode, scopedNotificationPublisher, scopeFileSystems, callContext, autoComplete) { } @@ -202,25 +188,24 @@ namespace Umbraco.Cms.Core.Scoping public Scope( ScopeProvider scopeProvider, CoreDebugSettings coreDebugSettings, - MediaFileManager mediaFileManager, IEventAggregator eventAggregator, ILogger logger, FileSystems fileSystems, Scope parent, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher notificationPublisher = null, bool? scopeFileSystems = null, bool callContext = false, bool autoComplete = false) - : this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, parent, - null, false, isolationLevel, repositoryCacheMode, eventDispatcher, notificationPublisher, + : this(scopeProvider, coreDebugSettings, eventAggregator, logger, fileSystems, parent, + null, false, isolationLevel, repositoryCacheMode, notificationPublisher, scopeFileSystems, callContext, autoComplete) { } internal Dictionary> ReadLocks => _readLocksDictionary; + internal Dictionary> WriteLocks => _writeLocksDictionary; // a value indicating whether to force call-context @@ -311,15 +296,6 @@ namespace Umbraco.Cms.Core.Scoping } } - public EventMessages MessagesOrNull - { - get - { - EnsureNotDisposed(); - return ParentScope == null ? _messages : ParentScope.MessagesOrNull; - } - } - // true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true" private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes; @@ -433,43 +409,6 @@ namespace Umbraco.Cms.Core.Scoping } } - /// - public EventMessages Messages - { - get - { - EnsureNotDisposed(); - if (ParentScope != null) - { - return ParentScope.Messages; - } - - return _messages ??= new EventMessages(); - - // TODO: event messages? - // this may be a problem: the messages collection will be cleared at the end of the scope - // how shall we process it in controllers etc? if we don't want the global factory from v7? - // it'd need to be captured by the controller - // - // + rename // EventMessages = ServiceMessages or something - } - } - - /// - public IEventDispatcher Events - { - get - { - EnsureNotDisposed(); - if (ParentScope != null) - { - return ParentScope.Events; - } - - return _eventDispatcher ??= new QueuingEventDispatcher(_mediaFileManager); - } - } - public IScopedNotificationPublisher Notifications { get @@ -840,7 +779,7 @@ namespace Umbraco.Cms.Core.Scoping completed = false; } - TryFinally(() => + void HandleScopedFileSystems() { if (_scopeFileSystem == true) { @@ -852,15 +791,17 @@ namespace Umbraco.Cms.Core.Scoping _fscope.Dispose(); _fscope = null; } - }, () => + } + + void HandleScopedNotifications() { - // deal with events if (onException == false) { - _eventDispatcher?.ScopeExit(completed); _notificationPublisher?.ScopeExit(completed); } - }, () => + } + + void HandleScopeContext() { // if *we* created it, then get rid of it if (_scopeProvider.AmbientContext == Context) @@ -875,7 +816,9 @@ namespace Umbraco.Cms.Core.Scoping _scopeProvider.PopAmbientScopeContext(); } } - }, () => + } + + void HandleDetachedScopes() { if (Detachable) { @@ -897,25 +840,35 @@ namespace Umbraco.Cms.Core.Scoping OrigScope = null; OrigContext = null; } - }); + } + + TryFinally( + HandleScopedFileSystems, + HandleScopedNotifications, + HandleScopeContext, + HandleDetachedScopes + ); } - private static void TryFinally(params Action[] actions) => TryFinally(0, actions); - - private static void TryFinally(int index, Action[] actions) + private static void TryFinally(params Action[] actions) { - if (index == actions.Length) + var exceptions = new List(); + + foreach (Action action in actions) { - return; + try + { + action(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } } - try + if (exceptions.Any()) { - actions[index](); - } - finally - { - TryFinally(index + 1, actions); + throw new AggregateException(exceptions); } } diff --git a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs index 4cc354cbad..0f5fba4d4a 100644 --- a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs @@ -11,6 +11,8 @@ using Umbraco.Extensions; using System.Collections.Generic; using System.Collections.Concurrent; using System.Threading; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; #if DEBUG_SCOPES using System.Linq; @@ -29,20 +31,24 @@ namespace Umbraco.Cms.Core.Scoping private readonly IRequestCache _requestCache; private readonly FileSystems _fileSystems; private CoreDebugSettings _coreDebugSettings; - private readonly MediaFileManager _mediaFileManager; private static readonly AsyncLocal> s_scopeStack = new AsyncLocal>(); private static readonly AsyncLocal> s_scopeContextStack = new AsyncLocal>(); private static readonly string s_scopeItemKey = typeof(Scope).FullName; private static readonly string s_contextItemKey = typeof(ScopeProvider).FullName; private readonly IEventAggregator _eventAggregator; - public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptionsMonitor coreDebugSettings, MediaFileManager mediaFileManager, ILogger logger, ILoggerFactory loggerFactory, IRequestCache requestCache, IEventAggregator eventAggregator) + public ScopeProvider( + IUmbracoDatabaseFactory databaseFactory, + FileSystems fileSystems, + IOptionsMonitor coreDebugSettings, + ILoggerFactory loggerFactory, + IRequestCache requestCache, + IEventAggregator eventAggregator) { DatabaseFactory = databaseFactory; _fileSystems = fileSystems; _coreDebugSettings = coreDebugSettings.CurrentValue; - _mediaFileManager = mediaFileManager; - _logger = logger; + _logger = loggerFactory.CreateLogger(); _loggerFactory = loggerFactory; _requestCache = requestCache; _eventAggregator = eventAggregator; @@ -56,6 +62,8 @@ namespace Umbraco.Cms.Core.Scoping public ISqlContext SqlContext => DatabaseFactory.SqlContext; + public IQuery CreateQuery() => SqlContext.Query(); + #region Context private void MoveHttpContextScopeToCallContext() @@ -279,10 +287,10 @@ namespace Umbraco.Cms.Core.Scoping #region Ambient Scope - IScope IScopeAccessor.AmbientScope => AmbientScope; + IDatabaseScope IScopeAccessor.AmbientScope => AmbientScope; /// - /// Get or set the Ambient (Current) for the current execution context. + /// Gets or set the Ambient (Current) for the current execution context. /// /// /// The current execution context may be request based (HttpContext) or on a background thread (AsyncLocal) @@ -381,10 +389,9 @@ namespace Umbraco.Cms.Core.Scoping public IScope CreateDetachedScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher scopedNotificationPublisher = null, bool? scopeFileSystems = null) - => new Scope(this, _coreDebugSettings, _mediaFileManager, _eventAggregator, _loggerFactory.CreateLogger(), _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopedNotificationPublisher, scopeFileSystems); + => new Scope(this, _coreDebugSettings, _eventAggregator, _loggerFactory.CreateLogger(), _fileSystems, true, null, isolationLevel, repositoryCacheMode, scopedNotificationPublisher, scopeFileSystems); /// public void AttachScope(IScope other, bool callContext = false) @@ -453,7 +460,6 @@ namespace Umbraco.Cms.Core.Scoping public IScope CreateScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher notificationPublisher = null, bool? scopeFileSystems = null, bool callContext = false, @@ -464,7 +470,7 @@ namespace Umbraco.Cms.Core.Scoping { IScopeContext ambientContext = AmbientContext; ScopeContext newContext = ambientContext == null ? new ScopeContext() : null; - var scope = new Scope(this, _coreDebugSettings, _mediaFileManager, _eventAggregator, _loggerFactory.CreateLogger(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, notificationPublisher, scopeFileSystems, callContext, autoComplete); + var scope = new Scope(this, _coreDebugSettings, _eventAggregator, _loggerFactory.CreateLogger(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, notificationPublisher, scopeFileSystems, callContext, autoComplete); // assign only if scope creation did not throw! PushAmbientScope(scope); if (newContext != null) @@ -474,7 +480,7 @@ namespace Umbraco.Cms.Core.Scoping return scope; } - var nested = new Scope(this, _coreDebugSettings, _mediaFileManager, _eventAggregator, _loggerFactory.CreateLogger(), _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, notificationPublisher, scopeFileSystems, callContext, autoComplete); + var nested = new Scope(this, _coreDebugSettings, _eventAggregator, _loggerFactory.CreateLogger(), _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, notificationPublisher, scopeFileSystems, callContext, autoComplete); PushAmbientScope(nested); return nested; } 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..4fff80ced6 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, @@ -104,7 +107,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 +117,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 +159,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 +169,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 +193,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 +418,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..420d66b0b4 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()) + { + } /// @@ -74,6 +112,9 @@ namespace Umbraco.Cms.Core.Security // create the member _memberService.Save(memberEntity); + //We need to add roles now that the member has an Id. It do not work implicit in UpdateMemberProperties + _memberService.AssignRoles(new[] { memberEntity.Id }, user.Roles.Select(x => x.RoleId).ToArray()); + if (!memberEntity.HasIdentity) { throw new DataException("Could not create the member, check logs for details"); @@ -83,18 +124,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 +194,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 +231,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 +252,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 +425,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 +539,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 +686,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..dd228ac008 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -14,25 +14,21 @@ namespace Umbraco.Cms.Infrastructure.Serialization Converters = new List() { new StringEnumConverter() - } + }, + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore }; - public string Serialize(object input) - { - return JsonConvert.SerializeObject(input, JsonSerializerSettings); - } - public T Deserialize(string input) - { - return JsonConvert.DeserializeObject(input, JsonSerializerSettings); - } + public string Serialize(object input) => JsonConvert.SerializeObject(input, JsonSerializerSettings); + + public T Deserialize(string input) => JsonConvert.DeserializeObject(input, JsonSerializerSettings); public T DeserializeSubset(string input, string key) { if (key == null) throw new ArgumentNullException(nameof(key)); - var root = JsonConvert.DeserializeObject(input); - - var jToken = root.SelectToken(key); + var root = Deserialize(input); + var jToken = root?.SelectToken(key); return jToken switch { diff --git a/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs index e4c17ab918..3cf23154c8 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -20,9 +20,10 @@ namespace Umbraco.Cms.Infrastructure.Serialization { return reader.Value; } + // 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/IdKeyMap.cs b/src/Umbraco.Infrastructure/Services/IdKeyMap.cs index 2acc57b372..0c6c57dbfd 100644 --- a/src/Umbraco.Infrastructure/Services/IdKeyMap.cs +++ b/src/Umbraco.Infrastructure/Services/IdKeyMap.cs @@ -1,23 +1,26 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Core.Services { public class IdKeyMap : IIdKeyMap,IDisposable { private readonly IScopeProvider _scopeProvider; + private readonly IScopeAccessor _scopeAccessor; private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); private readonly Dictionary> _id2Key = new Dictionary>(); private readonly Dictionary> _key2Id = new Dictionary>(); - public IdKeyMap(IScopeProvider scopeProvider) + public IdKeyMap(IScopeProvider scopeProvider, IScopeAccessor scopeAccessor) { _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; } // note - for pure read-only we might want to *not* enforce a transaction? @@ -170,11 +173,11 @@ namespace Umbraco.Cms.Core.Services //if it's unknown don't include the nodeObjectType in the query if (umbracoObjectType == UmbracoObjectTypes.Unknown) { - val = scope.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id", new { id = key}); + val = _scopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id", new { id = key}); } else { - val = scope.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", + val = _scopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", new { id = key, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Cms.Core.Constants.ObjectTypes.IdReservation }); } scope.Complete(); @@ -258,11 +261,11 @@ namespace Umbraco.Cms.Core.Services //if it's unknown don't include the nodeObjectType in the query if (umbracoObjectType == UmbracoObjectTypes.Unknown) { - val = scope.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id", new { id }); + val = _scopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id", new { id }); } else { - val = scope.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", + val = _scopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", new { id, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Cms.Core.Constants.ObjectTypes.IdReservation }); } scope.Complete(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index a37793207d..2f2d8b4c48 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -12,7 +12,6 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement @@ -109,7 +108,7 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - return ((EntityContainerRepository) _dataTypeContainerRepository).Get(containerId); + return _dataTypeContainerRepository.Get(containerId); } } @@ -117,7 +116,7 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - return ((EntityContainerRepository) _dataTypeContainerRepository).Get(name, level); + return _dataTypeContainerRepository.Get(name, level); } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs new file mode 100644 index 0000000000..cdcc6b19e9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs @@ -0,0 +1,151 @@ +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 IOptions _backOfficeIdentityOptions; + private readonly IDictionary _twoFactorSetupGenerators; + + /// + /// Initializes a new instance of the class. + /// + public TwoFactorLoginService( + ITwoFactorLoginRepository twoFactorLoginRepository, + IScopeProvider scopeProvider, + IEnumerable twoFactorSetupGenerators, + IOptions identityOptions, + IOptions backOfficeIdentityOptions + ) + { + _twoFactorLoginRepository = twoFactorLoginRepository; + _scopeProvider = scopeProvider; + _identityOptions = identityOptions; + _backOfficeIdentityOptions = backOfficeIdentityOptions; + _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x =>x.ProviderName); + } + + /// + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); + } + + /// + public async Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey) + { + return await GetEnabledProviderNamesAsync(userOrMemberKey); + } + + 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(IsKnownProviderName); + } + + /// + /// The provider needs to be registered as either a member provider or backoffice provider to show up. + /// + private bool IsKnownProviderName(string providerName) + { + if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) + { + return true; + } + + if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) + { + return true; + } + + return false; + } + + /// + public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) + { + return (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); + } + + /// + public async Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x => x.ProviderName == providerName)?.Secret; + } + + /// + 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 f4bb33c6c4..a953515bae 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/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs index c52c1271ad..60face6428 100644 --- a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Extensions { @@ -42,7 +43,7 @@ namespace Umbraco.Extensions // TODO: Gotta wonder how much this does actually improve perf? It's a lot of weird code to make this happen so hope it's worth it builder.Services.AddUnique(factory => { - var idkSvc = new IdKeyMap(factory.GetRequiredService()); + var idkSvc = new IdKeyMap(factory.GetRequiredService(), factory.GetRequiredService()); if (factory.GetRequiredService() is PublishedSnapshotService publishedSnapshotService) { idkSvc.SetMapper(UmbracoObjectTypes.Document, id => publishedSnapshotService.GetDocumentUid(id), uid => publishedSnapshotService.GetDocumentId(uid)); diff --git a/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs index 61f10917fd..47cc427217 100644 --- a/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs +++ b/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Umbraco.Cms.Core.PublishedCache; @@ -9,7 +10,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache { var assigned = domainCache.GetAssigned(documentId, includeWildcards); - return culture is null ? assigned.Any() : assigned.Any(x => x.Culture == culture); + // It's super important that we always compare cultures with ignore case, since we can't be sure of the casing! + return culture is null ? assigned.Any() : assigned.Any(x => x.Culture.Equals(culture, StringComparison.InvariantCultureIgnoreCase)); } } } diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index 649bc0eebb..8aa35c1608 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -18,6 +18,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; using Constants = Umbraco.Cms.Core.Constants; @@ -694,7 +695,7 @@ AND cmsContentNu.nodeId IS NULL } } - public ContentNodeKit GetMediaSource(IScope scope, int id) + public ContentNodeKit GetMediaSource(IDatabaseScope scope, int id) { var sql = SqlMediaSourcesSelect() .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) 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 75791fb283..c47a602f15 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/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 0221181cb3..f85da612ab 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -31,6 +32,7 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -68,7 +70,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; private readonly IManifestParser _manifestParser; private readonly ServerVariablesParser _serverVariables; + private readonly IOptions _securitySettings; + + [ActivatorUtilitiesConstructor] public BackOfficeController( IBackOfficeUserManager userManager, IRuntimeState runtimeState, @@ -87,7 +92,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IHttpContextAccessor httpContextAccessor, IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, IManifestParser manifestParser, - ServerVariablesParser serverVariables) + ServerVariablesParser serverVariables, + IOptions securitySettings) { _userManager = userManager; _runtimeState = runtimeState; @@ -107,6 +113,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; _manifestParser = manifestParser; _serverVariables = serverVariables; + _securitySettings = securitySettings; } [HttpGet] @@ -458,7 +465,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (response == null) throw new ArgumentNullException(nameof(response)); // Sign in the user with this external login provider (which auto links, etc...) - SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false); + SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false, bypassTwoFactor: _securitySettings.Value.UserBypassTwoFactorForExternalLogins); var errors = new List(); diff --git a/src/Umbraco.Web.BackOffice/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/HelpController.cs b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs index 3bc45703fa..f431b1a827 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs @@ -1,10 +1,17 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Net.Http; using System.Runtime.Serialization; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.DependencyInjection; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers @@ -13,15 +20,44 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public class HelpController : UmbracoAuthorizedJsonController { private readonly ILogger _logger; + private HelpPageSettings _helpPageSettings; + [Obsolete("Use constructor that takes IOptions")] public HelpController(ILogger logger) + : this(logger, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + [ActivatorUtilitiesConstructor] + public HelpController( + ILogger logger, + IOptionsMonitor helpPageSettings) { _logger = logger; + + ResetHelpPageSettings(helpPageSettings.CurrentValue); + helpPageSettings.OnChange(ResetHelpPageSettings); + } + + private void ResetHelpPageSettings(HelpPageSettings settings) + { + _helpPageSettings = settings; } private static HttpClient _httpClient; + public async Task> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.com") { + if (IsAllowedUrl(baseUrl) is false) + { + _logger.LogError($"The following URL is not listed in the allowlist for HelpPage in HelpPageSettings: {baseUrl}"); + HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + + // Ideally we'd want to return a BadRequestResult here, + // however, since we're not returning ActionResult this is not possible and changing it would be a breaking change. + return new List(); + } + var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); try @@ -44,6 +80,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return new List(); } + + private bool IsAllowedUrl(string url) + { + if (_helpPageSettings.HelpPageUrlAllowList is null || + _helpPageSettings.HelpPageUrlAllowList.Contains(url)) + { + return true; + } + + return false; + } } [DataContract(Name = "HelpPage")] diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 43518927ed..09cb7f97c9 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; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 012f910596..3f59d66a3f 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..65ebcf3bd2 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -2,9 +2,16 @@ 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.Infrastructure.Security; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Security; @@ -27,7 +34,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() @@ -62,5 +78,14 @@ namespace Umbraco.Extensions return umbracoBuilder; } + public static BackOfficeIdentityBuilder AddTwoFactorProvider(this BackOfficeIdentityBuilder identityBuilder, string providerName) where T : class, ITwoFactorProvider + { + identityBuilder.Services.AddSingleton(); + identityBuilder.Services.AddSingleton(); + identityBuilder.AddTokenProvider>(providerName); + + return identityBuilder; + } + } } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index aeb329efb4..63027a3c9e 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; @@ -54,7 +55,8 @@ namespace Umbraco.Extensions .AddCoreNotifications() .AddLogViewer() .AddExamine() - .AddExamineIndexes(); + .AddExamineIndexes() + .AddControllersWithAmbiguousConstructors(); public static IUmbracoBuilder AddUnattendedInstallInstallCreateUser(this IUmbracoBuilder builder) { @@ -82,12 +84,12 @@ namespace Umbraco.Extensions { builder.Services.AddSingleton(); builder.Services.ConfigureOptions(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.AddNotificationAsyncHandler(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -114,7 +116,21 @@ namespace Umbraco.Extensions }); builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddSingleton(); + + return builder; + } + + /// + /// Adds explicit registrations for controllers with ambiguous constructors to prevent downstream issues for + /// users who wish to use + /// + public static IUmbracoBuilder AddControllersWithAmbiguousConstructors( + this IUmbracoBuilder builder) + { + builder.Services.TryAddTransient(sp => + ActivatorUtilities.CreateInstance(sp)); 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..af8d0d877e --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Reflection; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Controllers; + +namespace Umbraco.Cms.Web.BackOffice.Services +{ + 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) + { + var potentialConflicting = controllers.Where(x => x.Name == controller.Name).ToArray(); + if (potentialConflicting.Length > 1) + { + //If we have any with same controller name and located in the same area, then it is a confict. + var conflicting = potentialConflicting + .Select(x => x.GetCustomAttribute()) + .GroupBy(x => x?.AreaName) + .Any(x => x?.Count() > 1); + + if (conflicting) + { + controllerName = controller.Name; + return true; + } + } + } + + controllerName = string.Empty; + return false; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index de480afdd2..74e8b10b23 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/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index 9e5919c1e2..13f73e1b41 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -37,7 +37,16 @@ namespace Umbraco.Cms.Web.Common.AspNetCore _webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); _urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode; - SiteName = webHostEnvironment.ApplicationName; + SetSiteName(hostingSettings.CurrentValue.SiteName); + + // We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack + // where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange. + // See summery of OptionsMonitorAdapter for more information. + if (hostingSettings is OptionsMonitor) + { + hostingSettings.OnChange(settings => SetSiteName(settings.SiteName)); + } + ApplicationPhysicalPath = webHostEnvironment.ContentRootPath; if (_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is not null) @@ -53,7 +62,7 @@ namespace Umbraco.Cms.Web.Common.AspNetCore public Uri ApplicationMainUrl { get; private set; } /// - public string SiteName { get; } + public string SiteName { get; private set; } /// public string ApplicationId @@ -198,7 +207,10 @@ namespace Umbraco.Cms.Web.Common.AspNetCore } } } + + private void SetSiteName(string siteName) => + SiteName = string.IsNullOrWhiteSpace(siteName) + ? _webHostEnvironment.ApplicationName + : siteName; } - - } 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 97a9f3e087..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); @@ -148,7 +147,7 @@ namespace Umbraco.Extensions // Add supported databases builder.AddUmbracoSqlServerSupport(); builder.AddUmbracoSqlCeSupport(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); // Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now builder.Services.AddSingleton(factory => new DbProviderFactoryCreator( @@ -200,7 +199,7 @@ namespace Umbraco.Extensions /// public static IUmbracoBuilder AddUmbracoProfiler(this IUmbracoBuilder builder) { - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddMiniProfiler(options => { @@ -286,7 +285,7 @@ namespace Umbraco.Extensions builder.Services.AddSmidgeInMemory(false); // it will be enabled based on config/cachebuster builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.ConfigureOptions(); @@ -332,7 +331,7 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); // register the umbraco context factory @@ -343,12 +342,12 @@ namespace Umbraco.Extensions builder.WithCollectionBuilder() .Add(umbracoApiControllerTypes); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddUnique(); + builder.Services.AddSingleton(); builder.Services.AddUnique(); builder.Services.AddUnique(); 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 d8dc5a66fe..b03528ec46 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/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index 7c5a89fa71..688414b3de 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -1,11 +1,16 @@ using System; using System.Linq; +using System.Net; using System.Threading; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; using StackExchange.Profiling; +using StackExchange.Profiling.Internal; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Profiler @@ -13,6 +18,8 @@ namespace Umbraco.Cms.Web.Common.Profiler public class WebProfiler : IProfiler { + private const string WebProfileCookieKey = "umbracoWebProfiler"; + public static readonly AsyncLocal MiniProfilerContext = new AsyncLocal(x => { _ = x; @@ -39,7 +46,6 @@ namespace Umbraco.Cms.Web.Common.Profiler public void Stop(bool discardResults = false) => MiniProfilerContext.Value?.Stop(discardResults); - public void UmbracoApplicationBeginRequest(HttpContext context, RuntimeLevel runtimeLevel) { if (runtimeLevel != RuntimeLevel.Run) @@ -50,9 +56,13 @@ namespace Umbraco.Cms.Web.Common.Profiler if (ShouldProfile(context.Request)) { Start(); + ICookieManager cookieManager = GetCookieManager(context); + cookieManager.ExpireCookie(WebProfileCookieKey); //Ensure we expire the cookie, so we do not reuse the old potential value saved } } + private static ICookieManager GetCookieManager(HttpContext context) => context.RequestServices.GetRequiredService(); + public void UmbracoApplicationEndRequest(HttpContext context, RuntimeLevel runtimeLevel) { if (runtimeLevel != RuntimeLevel.Run) @@ -70,19 +80,42 @@ namespace Umbraco.Cms.Web.Common.Profiler var first = Interlocked.Exchange(ref _first, 1) == 0; if (first) { - - var startupDuration = _startupProfiler.Root.DurationMilliseconds.GetValueOrDefault(); - MiniProfilerContext.Value.DurationMilliseconds += startupDuration; - MiniProfilerContext.Value.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; - MiniProfilerContext.Value.Root.AddChild(_startupProfiler.Root); + AddSubProfiler(_startupProfiler); _startupProfiler = null; } + + ICookieManager cookieManager = GetCookieManager(context); + var cookieValue = cookieManager.GetCookieValue(WebProfileCookieKey); + + if (cookieValue is not null) + { + AddSubProfiler(MiniProfiler.FromJson(cookieValue)); + } + + //If it is a redirect to a relative path (local redirect) + if (context.Response.StatusCode == (int)HttpStatusCode.Redirect + && context.Response.Headers.TryGetValue(Microsoft.Net.Http.Headers.HeaderNames.Location, out var location) + && !location.Contains("://")) + { + MiniProfilerContext.Value.Root.Name = "Before Redirect"; + cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson()); + } + } } } + private void AddSubProfiler(MiniProfiler subProfiler) + { + var startupDuration = subProfiler.Root.DurationMilliseconds.GetValueOrDefault(); + MiniProfilerContext.Value.DurationMilliseconds += startupDuration; + MiniProfilerContext.Value.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; + MiniProfilerContext.Value.Root.AddChild(subProfiler.Root); + + } + private static bool ShouldProfile(HttpRequest request) { if (request.IsClientSideRequest()) return false; 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 e73a15f989..79c4220ed9 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..d4272515e5 --- /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 + { + public 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/assets/img/application/umbraco_logo_large_blue.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg new file mode 100644 index 0000000000..ce15dd3092 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg index b27ae89e91..c0bdbdd40c 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg @@ -1,3 +1,41 @@ - - - + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg new file mode 100644 index 0000000000..b27ae89e91 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js index b52b0a5763..6cf6dd85f3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js @@ -1,9 +1,9 @@ (function () { "use strict"; - function AppHeaderDirective(eventsService, appState, userService, focusService, backdropService, overlayService) { + function AppHeaderDirective(eventsService, appState, userService, focusService, overlayService, $timeout) { - function link(scope, el, attr, ctrl) { + function link(scope, element) { var evts = []; @@ -84,6 +84,35 @@ overlayService.open(dialog); }; + scope.logoModal = { + show: false, + text: "", + timer: null + }; + scope.showLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + scope.logoModal.show = true; + scope.logoModal.text = "version "+Umbraco.Sys.ServerVariables.application.version; + $timeout(function () { + const anchorLink = element[0].querySelector('.umb-app-header__logo-modal'); + if(anchorLink) { + anchorLink.focus(); + } + }); + }; + scope.keepLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + }; + scope.hideLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + scope.logoModal.timer = $timeout(function () { + scope.logoModal.show = false; + }, 100); + }; + scope.stopClickEvent = function($event) { + $event.stopPropagation(); + }; + } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/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/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 22baed8472..09c1659775 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -101,11 +101,30 @@ /** * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. - * @param {Object} blockObject BlockObject to recive data values from. + * @param {Object} blockObject BlockObject to receive data values from. */ function getBlockLabel(blockObject) { if (blockObject.labelInterpolator !== undefined) { - var labelVars = Object.assign({"$contentTypeName": blockObject.content.contentTypeName, "$settings": blockObject.settingsData || {}, "$layout": blockObject.layout || {}, "$index": (blockObject.index || 0)+1 }, blockObject.data); + // blockobject.content may be null if the block is no longer allowed, + // so try and fall back to the label in the config, + // if that too is null, there's not much we can do, so just default to empty string. + var contentTypeName; + if(blockObject.content != null){ + contentTypeName = blockObject.content.contentTypeName; + } + else if(blockObject.config != null && blockObject.config.label != null){ + contentTypeName = blockObject.config.label; + } + else { + contentTypeName = ""; + } + + var labelVars = Object.assign({ + "$contentTypeName": contentTypeName, + "$settings": blockObject.settingsData || {}, + "$layout": blockObject.layout || {}, + "$index": (blockObject.index || 0)+1 + }, blockObject.data); var label = blockObject.labelInterpolator(labelVars); if (label) { return label; @@ -511,10 +530,10 @@ * @methodOf umbraco.services.blockEditorModelObject * @description Retrieve a Block Object for the given layout entry. * The Block Object offers the necessary data to display and edit a block. - * The Block Object setups live syncronization of content and settings models back to the data of your Property Editor model. - * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen. + * The Block Object setups live synchronization of content and settings models back to the data of your Property Editor model. + * The returned object, named ´BlockObject´, contains several useful models to make editing of this block happen. * The ´BlockObject´ contains the following properties: - * - key {string}: runtime generated key, usefull for tracking of this object + * - key {string}: runtime generated key, useful for tracking of this object * - content {Object}: Content model, the content data in a ElementType model. * - settings {Object}: Settings model, the settings data in a ElementType model. * - config {Object}: A local deep copy of the block configuration model. @@ -522,12 +541,11 @@ * - updateLabel {Method}: Method to trigger an update of the label for this block. * - data {Object}: A reference to the content data object from your property editor model. * - settingsData {Object}: A reference to the settings data object from your property editor model. - * - layout {Object}: A refernce to the layout entry from your property editor model. + * - layout {Object}: A reference to the layout entry from your property editor model. * @param {Object} layoutEntry the layout entry object to build the block model from. - * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block. + * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasn't found for this block. */ getBlockObject: function (layoutEntry) { - var contentUdi = layoutEntry.contentUdi; var dataModel = getDataByUdi(contentUdi, this.value.contentData); 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/application/umb-app-header.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less index 68a29df89e..bb346fc402 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less @@ -2,12 +2,65 @@ background: @blueExtraDark; display: flex; align-items: center; - justify-content: space-between; max-width: 100%; height: @appHeaderHeight; padding: 0 20px; } +.umb-app-header__logo { + margin-right: 30px; + button { + img { + height: 30px; + } + } +} + +.umb-app-header__logo-modal { + position: absolute; + z-index: @zindexUmbOverlay; + top: 50px; + left: 17px; + font-size: 13px; + + border-radius: 6px; + + width: 160px; + padding: 20px 20px; + background-color:@white; + color: @blueExtraDark; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .14), 0 1px 6px 1px rgba(0, 0, 0, .14); + text-decoration: none; + + text-align: center; + + &::before { + content:''; + position: absolute; + transform: rotate(45deg); + background-color:@white; + top: -4px; + left: 14px; + width: 8px; + height: 8px; + } + + img { + display: block; + height: auto; + width: 120px; + margin-left: auto; + margin-right: auto; + margin-bottom: 3px; + } +} + +.umb-app-header__right { + display: flex; + align-items: center; + margin-left: auto; +} + .umb-app-header__actions { display: flex; list-style: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/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..015c291564 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -28,12 +28,15 @@ .login-overlay__logo { position: absolute; - top: 22px; - left: 25px; - width: 30px; + top: 12.5px; + left: 20px; + right: 25px; height: 30px; z-index: 1; } +.login-overlay__logo img { + height: 100%; +} .login-overlay .umb-modalcolumn { background: none; @@ -66,7 +69,8 @@ margin-right: 25px; margin-top: auto; margin-bottom: auto; - border-radius: @baseBorderRadius; + border-radius: @doubleBorderRadius; + box-shadow: 0 1px 6px 1px rgba(0, 0, 0, 0.12); } .login-overlay .form input[type="text"], diff --git a/src/Umbraco.Web.UI.Client/src/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/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index e0fb4aeb77..98b8d88869 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -1,34 +1,99 @@ -
-
- - + -
+ + +
  • -
  • -
  • -
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/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/views/errors/BootFailed.html b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html index c08627739a..7b91125e09 100644 --- a/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html +++ b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html @@ -1,79 +1,87 @@ - - - - Boot Failed - - - -
- -
-

Boot Failed

-

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

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

Boot Failed

+

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

+
+
+ 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/Composers/ControllersAsServicesComposer.cs b/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs new file mode 100644 index 0000000000..042343df67 --- /dev/null +++ b/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Web.UI.Composers +{ + /// + /// Adds controllers to the service collection. + /// + /// + /// + /// Umbraco 9 out of the box, makes use of which doesn't resolve controller + /// instances from the IOC container, instead it resolves the required dependencies of the controller and constructs an instance + /// of the controller. + /// + /// + /// Some users may wish to switch to (perhaps to make use of interception/decoration). + /// + /// + /// This composer exists to help us detect ambiguous constructors in the CMS such that we do not cause unnecessary effort downstream. + /// + /// + /// This Composer is not shipped by the Umbraco.Templates package. + /// + /// + public class ControllersAsServicesComposer : IComposer + { + /// + public void Compose(IUmbracoBuilder builder) => builder.Services + .AddMvc() + .AddControllersAsServicesWithoutChangingActivator(); + } + + internal static class MvcBuilderExtensions + { + /// + /// but without the replacement of + /// . + /// + /// + /// We don't need to opt in to to ensure container validation + /// passes. + /// + public static IMvcBuilder AddControllersAsServicesWithoutChangingActivator(this IMvcBuilder builder) + { + var feature = new ControllerFeature(); + builder.PartManager.PopulateFeature(feature); + + foreach (Type controller in feature.Controllers.Select(c => c.AsType())) + { + builder.Services.TryAddTransient(controller, controller); + } + + return builder; + } + } +} diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index d9f98e4443..c055651efc 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -17,6 +17,16 @@ + + + + + + + @@ -93,7 +103,7 @@ - + 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..cb9188f5d0 --- /dev/null +++ b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs @@ -0,0 +1,289 @@ +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 Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Routing; +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 IOptions _securitySettings; + 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, + IOptions securitySettings) + : base( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider) + { + _logger = logger; + _memberSignInManager = memberSignInManager; + _memberManager = memberManager; + _twoFactorLoginService = twoFactorLoginService; + _securitySettings = securitySettings; + } + + /// + /// 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, _securitySettings.Value.MemberBypassTwoFactorForExternalLogins); + + 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/stylecop.json b/stylecop.json deleted file mode 100644 index b2f7771470..0000000000 --- a/stylecop.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", - "settings": { - "orderingRules": { - "usingDirectivesPlacement": "outsideNamespace", - "elementOrder": [ - "kind" - ] - }, - "documentationRules": { - "xmlHeader": false, - "documentInternalElements": false, - "copyrightText": "Copyright (c) Umbraco.\nSee LICENSE for more details." - } - } -} 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/DependencyInjection/UmbracoBuilderExtensions.cs b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs index 7bf435485f..b5c917a337 100644 --- a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs @@ -39,9 +39,9 @@ namespace Umbraco.Cms.Tests.Integration.DependencyInjection /// /// Uses/Replaces services with testing services /// - public static IUmbracoBuilder AddTestServices(this IUmbracoBuilder builder, TestHelper testHelper, AppCaches appCaches = null) + public static IUmbracoBuilder AddTestServices(this IUmbracoBuilder builder, TestHelper testHelper) { - builder.Services.AddUnique(appCaches ?? AppCaches.NoCache); + builder.Services.AddUnique(AppCaches.NoCache); builder.Services.AddUnique(Mock.Of()); builder.Services.AddUnique(testHelper.MainDom); 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..8c7eabadde 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -1,6 +1,3 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - using System; using System.Linq.Expressions; using System.Net.Http; @@ -12,13 +9,15 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Moq; using NUnit.Framework; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.DependencyInjection; @@ -32,14 +31,17 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest { [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console, Boot = true)] - public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTest + public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTestBase { - [SetUp] - public override void Setup() - { - InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = null; - InMemoryConfiguration["Umbraco:CMS:Hosting:Debug"] = "true"; + protected HttpClient Client { get; private set; } + protected LinkGenerator LinkGenerator { get; private set; } + + protected WebApplicationFactory Factory { get; private set; } + + [SetUp] + public void Setup() + { /* * It's worth noting that our usage of WebApplicationFactory is non-standard, * the intent is that your Startup.ConfigureServices is called just like @@ -50,9 +52,12 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest * This is currently a pain to refactor towards due to UmbracoBuilder+TypeFinder+TypeLoader setup but * we should get there one day. * + * However we need to separate the testing framework we provide for downstream projects from our own tests. + * We cannot use the Umbraco.Web.UI startup yet as that is not available downstream. + * * See https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests */ - var factory = new UmbracoWebApplicationFactory(CreateHostBuilder, BeforeHostStart); + var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); // additional host configuration for web server integration tests Factory = factory.WithWebHostBuilder(builder => @@ -68,7 +73,6 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .AddScheme(TestAuthHandler.TestAuthenticationScheme, options => { })); }); - Client = Factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false @@ -77,21 +81,6 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest LinkGenerator = Factory.Services.GetRequiredService(); } - public override IHostBuilder CreateHostBuilder() - { - IHostBuilder builder = base.CreateHostBuilder(); - builder.ConfigureWebHost(builder => - { - // need to configure the IWebHostEnvironment too - builder.ConfigureServices((c, s) => c.HostingEnvironment = TestHelper.GetWebHostEnvironment()); - - // call startup - builder.Configure(app => Configure(app)); - }).UseEnvironment(Environments.Development); - - return builder; - } - /// /// Prepare a url before using . /// This returns the url but also sets the HttpContext.request into to use this url. @@ -100,7 +89,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest protected string PrepareApiControllerUrl(Expression> methodSelector) where T : UmbracoApiController { - string url = LinkGenerator.GetUmbracoApiService(methodSelector); + var url = LinkGenerator.GetUmbracoApiService(methodSelector); return PrepareUrl(url); } @@ -112,7 +101,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest protected string PrepareSurfaceControllerUrl(Expression> methodSelector) where T : SurfaceController { - string url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); + var url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); return PrepareUrl(url); } @@ -142,17 +131,61 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest return url; } - protected HttpClient Client { get; private set; } - - protected LinkGenerator LinkGenerator { get; private set; } - - protected WebApplicationFactory Factory { get; private set; } - - public override void ConfigureServices(IServiceCollection services) + private IHostBuilder CreateHostBuilder() { + IHostBuilder hostBuilder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, configBuilder) => + { + context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + configBuilder.Sources.Clear(); + configBuilder.AddInMemoryCollection(InMemoryConfiguration); + + Configuration = configBuilder.Build(); + }) + .ConfigureWebHost(builder => + { + builder.ConfigureServices((context, services) => + { + context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + + ConfigureServices(services); + ConfigureTestServices(services); + + if (!TestOptions.Boot) + { + // If boot is false, we don't want the CoreRuntime hosted service to start + // So we replace it with a Mock + services.AddUnique(Mock.Of()); + } + }); + + // call startup + builder.Configure(Configure); + }) + .UseDefaultServiceProvider(cfg => + { + // These default to true *if* WebHostEnvironment.EnvironmentName == Development + // When running tests, EnvironmentName used to be null on the mock that we register into services. + // Enable opt in for tests so that validation occurs regardless of environment name. + // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. + cfg.ValidateOnBuild = true; + cfg.ValidateScopes = true; + }); + + return hostBuilder; + } + + protected virtual IServiceProvider Services => Factory.Services; + + protected virtual T GetRequiredService() => Factory.Services.GetRequiredService(); + + protected void ConfigureServices(IServiceCollection services) + { + services.AddUnique(CreateLoggerFactory()); services.AddTransient(); Core.Hosting.IHostingEnvironment hostingEnvironment = TestHelper.GetHostingEnvironment(); + TypeLoader typeLoader = services.AddTypeLoader( GetType().Assembly, hostingEnvironment, @@ -195,8 +228,17 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .Build(); } - public override void Configure(IApplicationBuilder app) + /// + /// Hook for registering test doubles. + /// + protected virtual void ConfigureTestServices(IServiceCollection services) { + } + + protected void Configure(IApplicationBuilder app) + { + UseTestDatabase(app); + app.UseUmbraco() .WithMiddleware(u => { diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs index 380603ae5c..62d84a19a1 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs @@ -18,11 +18,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest /// /// Method to create the IHostBuilder /// Method to perform an action before IHost starts - public UmbracoWebApplicationFactory(Func createHostBuilder, Action beforeStart = null) - { - _createHostBuilder = createHostBuilder; - _beforeStart = beforeStart; - } + public UmbracoWebApplicationFactory(Func createHostBuilder) => _createHostBuilder = createHostBuilder; protected override IHostBuilder CreateHostBuilder() => _createHostBuilder(); 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 314a614b49..dd9fd3fe4e 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -1,23 +1,12 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - 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.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; -using Serilog; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; @@ -29,13 +18,11 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.DependencyInjection; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Extensions; -using Umbraco.Cms.Tests.Integration.Implementations; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Testing @@ -46,123 +33,35 @@ namespace Umbraco.Cms.Tests.Integration.Testing /// /// This will use a Host Builder to boot and install Umbraco ready for use /// - [SingleThreaded] - [NonParallelizable] - public abstract class UmbracoIntegrationTest + public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase { - private List _testTeardown = null; - private readonly List _fixtureTeardown = new List(); + private IHost _host; - public void OnTestTearDown(Action tearDown) - { - if (_testTeardown == null) - { - _testTeardown = new List(); - } - - _testTeardown.Add(tearDown); - } - - public void OnFixtureTearDown(Action tearDown) => _fixtureTeardown.Add(tearDown); - - [OneTimeTearDown] - public void FixtureTearDown() - { - foreach (Action a in _fixtureTeardown) - { - a(); - } - } - - [TearDown] - public async Task TearDownAsync() - { - if (_testTeardown != null) - { - foreach (Action a in _testTeardown) - { - a(); - } - } - - _testTeardown = null; - _firstTestInFixture = false; - s_firstTestInSession = false; - - // Ensure CoreRuntime stopped (now it's a HostedService) - IHost host = Services?.GetService(); - if (host is not null) - { - await host.StopAsync(); - host.Dispose(); - } - - } - - [TearDown] - public virtual void TearDown_Logging() => - TestContext.Progress.Write($" {TestContext.CurrentContext.Result.Outcome.Status}"); + protected IServiceProvider Services => _host.Services; [SetUp] - public virtual void SetUp_Logging() => - TestContext.Progress.Write($"Start test {s_testCount++}: {TestContext.CurrentContext.Test.Name}"); - - [SetUp] - public virtual void Setup() + public void Setup() { InMemoryConfiguration[Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true"; IHostBuilder hostBuilder = CreateHostBuilder(); - IHost host = hostBuilder.Build(); - BeforeHostStart(host); - host.Start(); + _host = hostBuilder.Build(); + UseTestDatabase(_host.Services); + _host.Start(); - var app = new ApplicationBuilder(host.Services); - Configure(app); - } - - protected virtual void BeforeHostStart(IHost host) - { - Services = host.Services; - UseTestDatabase(Services); - } - - private ILoggerFactory CreateLoggerFactory() - { - try + if (TestOptions.Boot) { - switch (TestOptions.Logger) - { - case UmbracoTestOptions.Logger.Mock: - return NullLoggerFactory.Instance; - case UmbracoTestOptions.Logger.Serilog: - return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => - { - string path = Path.Combine(TestHelper.WorkingDirectory, "logs", "umbraco_integration_tests_.txt"); - - Log.Logger = new LoggerConfiguration() - .WriteTo.File(path, rollingInterval: RollingInterval.Day) - .MinimumLevel.Debug() - .CreateLogger(); - - builder.AddSerilog(Log.Logger); - }); - case UmbracoTestOptions.Logger.Console: - return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug)); - } + Services.GetRequiredService().EnsureUmbracoContext(); } - catch - { - // ignored - } - - return NullLoggerFactory.Instance; } + [TearDown] + public void TearDownAsync() => _host.StopAsync(); + /// /// Create the Generic Host and execute startup ConfigureServices/Configure calls /// - public virtual IHostBuilder CreateHostBuilder() + private IHostBuilder CreateHostBuilder() { IHostBuilder hostBuilder = Host.CreateDefaultBuilder() @@ -170,7 +69,6 @@ namespace Umbraco.Cms.Tests.Integration.Testing // if you want to setup your tests this way, it is a bit annoying to do that as the WebApplicationFactory will // create separate Host instances. So instead of UseStartup, we just call ConfigureServices/Configure ourselves, // and in the case of the UmbracoTestServerTestBase it will use the ConfigureWebHost to Configure the IApplicationBuilder directly. - // .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(GetType()); }) .ConfigureAppConfiguration((context, configBuilder) => { context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); @@ -179,10 +77,10 @@ namespace Umbraco.Cms.Tests.Integration.Testing Configuration = configBuilder.Build(); }) - .ConfigureServices((hostContext, services) => + .ConfigureServices((_, services) => { ConfigureServices(services); - services.AddUnique(CreateLoggerFactory()); + ConfigureTestServices(services); if (!TestOptions.Boot) { @@ -191,16 +89,21 @@ namespace Umbraco.Cms.Tests.Integration.Testing services.AddUnique(Mock.Of()); } }); + return hostBuilder; } - public virtual void ConfigureServices(IServiceCollection services) + protected void ConfigureServices(IServiceCollection services) { + services.AddUnique(CreateLoggerFactory()); services.AddSingleton(TestHelper.DbProviderFactoryCreator); services.AddTransient(); 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( @@ -222,7 +125,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing .AddBackOfficeIdentity() .AddMembersIdentity() .AddExamine() - .AddTestServices(TestHelper, GetAppCaches()); + .AddTestServices(TestHelper); if (TestOptions.Mapper) { @@ -240,182 +143,25 @@ namespace Umbraco.Cms.Tests.Integration.Testing builder.Build(); } - protected virtual AppCaches GetAppCaches() => - - // Disable caches for integration tests - AppCaches.NoCache; - - public virtual void Configure(IApplicationBuilder app) - { - if (TestOptions.Boot) - { - Services.GetRequiredService().EnsureUmbracoContext(); - } - - app.UseUmbracoCore(); // This no longer starts CoreRuntime, it's very fast - } - - private static readonly object s_dbLocker = new object(); - private static ITestDatabase s_dbInstance; - private static TestDbMeta s_fixtureDbMeta; - - protected void UseTestDatabase(IServiceProvider serviceProvider) - { - IRuntimeState state = serviceProvider.GetRequiredService(); - TestUmbracoDatabaseFactoryProvider testDatabaseFactoryProvider = serviceProvider.GetRequiredService(); - IUmbracoDatabaseFactory databaseFactory = serviceProvider.GetRequiredService(); - ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); - - // This will create a db, install the schema and ensure the app is configured to run - SetupTestDatabase(testDatabaseFactoryProvider, databaseFactory, loggerFactory, state, TestHelper.WorkingDirectory); - } - /// - /// Get or create an instance of + /// Hook for altering UmbracoBuilder setup /// /// - /// There must only be ONE instance shared between all tests in a session + /// Can also be used for registering test doubles. /// - private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) + protected virtual void CustomTestSetup(IUmbracoBuilder builder) { - lock (s_dbLocker) - { - if (s_dbInstance != null) - { - return s_dbInstance; - } - - // TODO: pull from IConfiguration - var settings = new TestDatabaseSettings - { - PrepareThreadCount = 4, - EmptyDatabasesCount = 2, - SchemaDatabaseCount = 4 - }; - - s_dbInstance = TestDatabaseFactory.Create(settings, filesPath, loggerFactory, dbFactory); - - return s_dbInstance; - } } /// - /// Creates a LocalDb instance to use for the test + /// Hook for registering test doubles. /// - private void SetupTestDatabase( - TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, - IUmbracoDatabaseFactory databaseFactory, - ILoggerFactory loggerFactory, - IRuntimeState runtimeState, - string workingDirectory) + protected virtual void ConfigureTestServices(IServiceCollection services) { - if (TestOptions.Database == UmbracoTestOptions.Database.None) - { - return; - } - - // need to manually register this factory - DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - string dbFilePath = Path.Combine(workingDirectory, "LocalDb"); - - ITestDatabase db = GetOrCreateDatabase(dbFilePath, loggerFactory, testUmbracoDatabaseFactoryProvider); - - switch (TestOptions.Database) - { - case UmbracoTestOptions.Database.NewSchemaPerTest: - - // New DB + Schema - TestDbMeta newSchemaDbMeta = db.AttachSchema(); - - // Add teardown callback - OnTestTearDown(() => db.Detach(newSchemaDbMeta)); - - ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState); - - Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); - - break; - case UmbracoTestOptions.Database.NewEmptyPerTest: - TestDbMeta newEmptyDbMeta = db.AttachEmpty(); - - // Add teardown callback - OnTestTearDown(() => db.Detach(newEmptyDbMeta)); - - ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState); - - Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); - - break; - case UmbracoTestOptions.Database.NewSchemaPerFixture: - // Only attach schema once per fixture - // Doing it more than once will block the process since the old db hasn't been detached - // and it would be the same as NewSchemaPerTest even if it didn't block - if (_firstTestInFixture) - { - // New DB + Schema - TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema(); - s_fixtureDbMeta = newSchemaFixtureDbMeta; - - // Add teardown callback - OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); - } - - ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); - - break; - case UmbracoTestOptions.Database.NewEmptyPerFixture: - // Only attach schema once per fixture - // Doing it more than once will block the process since the old db hasn't been detached - // and it would be the same as NewSchemaPerTest even if it didn't block - if (_firstTestInFixture) - { - // New DB + Schema - TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty(); - s_fixtureDbMeta = newEmptyFixtureDbMeta; - - // Add teardown callback - OnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); - } - - ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); - - break; - default: - throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); - } } - private void ConfigureTestDatabaseFactory(TestDbMeta meta, IUmbracoDatabaseFactory factory, IRuntimeState state) - { - ILogger log = Services.GetRequiredService>(); - log.LogInformation($"ConfigureTestDatabaseFactory - Using test database: [{meta.Name}] - IsEmpty: [{meta.IsEmpty}]"); - - // It's just been pulled from container and wasn't used to create test database - Assert.IsFalse(factory.Configured); - - factory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); - state.DetermineRuntimeLevel(); - log.LogInformation($"ConfigureTestDatabaseFactory - Determined RuntimeLevel: [{state.Level}]"); - } - - protected UmbracoTestAttribute TestOptions => TestOptionAttributeBase.GetTestOptions(); - protected virtual T GetRequiredService() => Services.GetRequiredService(); - public Dictionary InMemoryConfiguration { get; } = new Dictionary(); - - public IConfiguration Configuration { get; protected set; } - - public TestHelper TestHelper { get; } = new TestHelper(); - - protected virtual void CustomTestSetup(IUmbracoBuilder builder) { } - - /// - /// Gets or sets the DI container. - /// - protected IServiceProvider Services { get; set; } - /// /// Gets the /// @@ -441,11 +187,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing protected IMapperCollection Mappers => Services.GetRequiredService(); - protected UserBuilder UserBuilderInstance { get; } = new UserBuilder(); - protected UserGroupBuilder UserGroupBuilderInstance { get; } = new UserGroupBuilder(); + protected UserBuilder UserBuilderInstance { get; } = new (); - private static bool s_firstTestInSession = true; - private bool _firstTestInFixture = true; - private static int s_testCount = 1; + protected UserGroupBuilder UserGroupBuilderInstance { get; } = new (); } } diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs new file mode 100644 index 0000000000..b77ec1806f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; +using Serilog; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Implementations; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +/// +/// Base class for all UmbracoIntegrationTests +/// +[SingleThreaded] +[NonParallelizable] +public abstract class UmbracoIntegrationTestBase +{ + private static readonly object s_dbLocker = new (); + private static ITestDatabase s_dbInstance; + private static TestDbMeta s_fixtureDbMeta; + private static int s_testCount = 1; + + private bool _firstTestInFixture = true; + private readonly Queue _testTeardown = new (); + private readonly List _fixtureTeardown = new (); + + protected Dictionary InMemoryConfiguration { get; } = new (); + + protected IConfiguration Configuration { get; set; } + + protected UmbracoTestAttribute TestOptions => + TestOptionAttributeBase.GetTestOptions(); + + protected TestHelper TestHelper { get; } = new (); + + private void AddOnTestTearDown(Action tearDown) => _testTeardown.Enqueue(tearDown); + + private void AddOnFixtureTearDown(Action tearDown) => _fixtureTeardown.Add(tearDown); + + [SetUp] + public void SetUp_Logging() => + TestContext.Progress.Write($"Start test {s_testCount++}: {TestContext.CurrentContext.Test.Name}"); + + [TearDown] + public void TearDown_Logging() => + TestContext.Progress.Write($" {TestContext.CurrentContext.Result.Outcome.Status}"); + + [OneTimeTearDown] + public void FixtureTearDown() + { + foreach (Action a in _fixtureTeardown) + { + a(); + } + } + + [TearDown] + public void TearDown() + { + _firstTestInFixture = false; + + while (_testTeardown.TryDequeue(out Action a)) + { + a(); + } + } + + protected ILoggerFactory CreateLoggerFactory() + { + try + { + switch (TestOptions.Logger) + { + case UmbracoTestOptions.Logger.Mock: + return NullLoggerFactory.Instance; + case UmbracoTestOptions.Logger.Serilog: + return LoggerFactory.Create(builder => + { + var path = Path.Combine(TestHelper.WorkingDirectory, "logs", "umbraco_integration_tests_.txt"); + + Log.Logger = new LoggerConfiguration() + .WriteTo.File(path, rollingInterval: RollingInterval.Day) + .MinimumLevel.Debug() + .CreateLogger(); + + builder.AddSerilog(Log.Logger); + }); + case UmbracoTestOptions.Logger.Console: + return LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug)); + } + } + catch + { + // ignored + } + + return NullLoggerFactory.Instance; + } + + protected void UseTestDatabase(IApplicationBuilder app) + => UseTestDatabase(app.ApplicationServices); + + protected void UseTestDatabase(IServiceProvider serviceProvider) + { + IRuntimeState state = serviceProvider.GetRequiredService(); + TestUmbracoDatabaseFactoryProvider testDatabaseFactoryProvider = serviceProvider.GetRequiredService(); + IUmbracoDatabaseFactory databaseFactory = serviceProvider.GetRequiredService(); + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + + // This will create a db, install the schema and ensure the app is configured to run + SetupTestDatabase(testDatabaseFactoryProvider, databaseFactory, loggerFactory, state, TestHelper.WorkingDirectory); + } + + private void ConfigureTestDatabaseFactory(TestDbMeta meta, IUmbracoDatabaseFactory factory, IRuntimeState state) + { + // It's just been pulled from container and wasn't used to create test database + Assert.IsFalse(factory.Configured); + + factory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); + state.DetermineRuntimeLevel(); + } + + private void SetupTestDatabase( + TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, + IUmbracoDatabaseFactory databaseFactory, + ILoggerFactory loggerFactory, + IRuntimeState runtimeState, + string workingDirectory) + { + if (TestOptions.Database == UmbracoTestOptions.Database.None) + { + return; + } + + // need to manually register this factory + DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + + var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); + + ITestDatabase db = GetOrCreateDatabase(dbFilePath, loggerFactory, testUmbracoDatabaseFactoryProvider); + + switch (TestOptions.Database) + { + case UmbracoTestOptions.Database.NewSchemaPerTest: + + // New DB + Schema + TestDbMeta newSchemaDbMeta = db.AttachSchema(); + + // Add teardown callback + AddOnTestTearDown(() => db.Detach(newSchemaDbMeta)); + + ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState); + + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewEmptyPerTest: + TestDbMeta newEmptyDbMeta = db.AttachEmpty(); + + // Add teardown callback + AddOnTestTearDown(() => db.Detach(newEmptyDbMeta)); + + ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState); + + Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewSchemaPerFixture: + // Only attach schema once per fixture + // Doing it more than once will block the process since the old db hasn't been detached + // and it would be the same as NewSchemaPerTest even if it didn't block + if (_firstTestInFixture) + { + // New DB + Schema + TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema(); + s_fixtureDbMeta = newSchemaFixtureDbMeta; + + // Add teardown callback + AddOnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); + } + + ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); + + break; + case UmbracoTestOptions.Database.NewEmptyPerFixture: + // Only attach schema once per fixture + // Doing it more than once will block the process since the old db hasn't been detached + // and it would be the same as NewSchemaPerTest even if it didn't block + if (_firstTestInFixture) + { + // New DB + Schema + TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty(); + s_fixtureDbMeta = newEmptyFixtureDbMeta; + + // Add teardown callback + AddOnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); + } + + ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); + + break; + default: + throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); + } + } + + private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) + { + lock (s_dbLocker) + { + if (s_dbInstance != null) + { + return s_dbInstance; + } + + // TODO: pull from IConfiguration + var settings = new TestDatabaseSettings + { + PrepareThreadCount = 4, + EmptyDatabasesCount = 2, + SchemaDatabaseCount = 4 + }; + + s_dbInstance = TestDatabaseFactory.Create(settings, filesPath, loggerFactory, dbFactory); + + return s_dbInstance; + } + } +} 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..4f7db59791 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs @@ -0,0 +1,70 @@ +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 + { + protected override void ConfigureTestServices(IServiceCollection 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.Core/RuntimeStateTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs index 52fade8dc2..d0d90c5726 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs @@ -24,14 +24,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class RuntimeStateTests : UmbracoIntegrationTest { - private protected IRuntimeState RuntimeState { get; private set; } - - public override void Configure(IApplicationBuilder app) - { - base.Configure(app); - - RuntimeState = Services.GetRequiredService(); - } + private IRuntimeState RuntimeState => Services.GetRequiredService(); protected override void CustomTestSetup(IUmbracoBuilder builder) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs index a2a82d57fb..95b708f167 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs @@ -29,11 +29,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine protected IRuntimeState RunningRuntimeState { get; } = Mock.Of(x => x.Level == RuntimeLevel.Run); - public override void ConfigureServices(IServiceCollection services) - { - base.ConfigureServices(services); - services.AddSingleton(); - } + protected override void ConfigureTestServices(IServiceCollection services) + => services.AddSingleton(); /// /// Used to create and manage a testable index @@ -88,7 +85,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine scopeProviderMock.Setup(x => x.CreateScope( It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index 40dbad176e..19d4080524 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -56,7 +56,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); - var helper = new DatabaseSchemaCreator(scope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); bool exists = helper.TableExists("umbracoUser"); Assert.IsTrue(exists); 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/Packaging/PackageDataInstallationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/PackageDataInstallationTests.cs index ef6585bef6..3feac1ce38 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/PackageDataInstallationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/PackageDataInstallationTests.cs @@ -150,7 +150,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Packaging using IScope scope = ScopeProvider.CreateScope(); foreach (IPropertyType propertyType in mRBasePage.PropertyTypes) { - PropertyTypeDto propertyTypeDto = scope.Database.First("WHERE id = @id", new { id = propertyType.Id }); + PropertyTypeDto propertyTypeDto = ScopeAccessor.AmbientScope.Database.First("WHERE id = @id", new { id = propertyType.Id }); Assert.AreEqual(propertyTypeDto.UniqueId, propertyType.Key); } } @@ -424,7 +424,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Packaging string configuration; using (IScope scope = ScopeProvider.CreateScope()) { - List dtos = scope.Database.Fetch("WHERE nodeId = @Id", new { dataTypeDefinitions.First().Id }); + List dtos = ScopeAccessor.AmbientScope.Database.Fetch("WHERE nodeId = @Id", new { dataTypeDefinitions.First().Id }); configuration = dtos.Single().Configuration; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index 96ded92f41..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; 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; @@ -27,7 +26,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence // create a few lock objects using (var scope = ScopeProvider.CreateScope()) { - var database = scope.Database; + var database = ScopeAccessor.AmbientScope.Database; database.Insert("umbracoLock", "id", false, new LockDto { Id = 1, Name = "Lock.1" }); database.Insert("umbracoLock", "id", false, new LockDto { Id = 2, Name = "Lock.2" }); database.Insert("umbracoLock", "id", false, new LockDto { Id = 3, Name = "Lock.3" }); @@ -121,7 +120,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence using (var scope = ScopeProvider.CreateScope()) { - var db = scope.Database; + var db = ScopeAccessor.AmbientScope.Database; try { db.EnableSqlCount = true; @@ -297,7 +296,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence Console.WriteLine($"[{id1}] WAIT {id1}"); scope.EagerWriteLock(id1); Console.WriteLine($"[{id1}] GRANT {id1}"); - WriteLocks(scope.Database); + WriteLocks(ScopeAccessor.AmbientScope.Database); myEv.Set(); if (id1 == 1) @@ -312,7 +311,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence Console.WriteLine($"[{id1}] WAIT {id2}"); scope.EagerWriteLock(id2); Console.WriteLine($"[{id1}] GRANT {id2}"); - WriteLocks(scope.Database); + WriteLocks(ScopeAccessor.AmbientScope.Database); } catch (Exception e) { @@ -483,7 +482,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var realDb = (Database)scope.Database; + var realDb = (Database)ScopeAccessor.AmbientScope.Database; realDb.CommandTimeout = 1000; Console.WriteLine("Write lock A"); @@ -505,7 +504,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence Console.WriteLine($"[{id}] WAIT {id}"); scope.EagerWriteLock(id); Console.WriteLine($"[{id}] GRANT {id}"); - WriteLocks(scope.Database); + WriteLocks(ScopeAccessor.AmbientScope.Database); myEv.Set(); otherEv.WaitOne(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoBulkInsertTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoBulkInsertTests.cs index 7fd90c97b3..10b0a06032 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoBulkInsertTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoBulkInsertTests.cs @@ -39,7 +39,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco using (IScope scope = ScopeProvider.CreateScope()) { // Still no what we want, but look above. - IUmbracoDatabase dbSqlServer = scope.Database; + IUmbracoDatabase dbSqlServer = ScopeAccessor.AmbientScope.Database; // drop the table dbSqlServer.Execute("DROP TABLE [umbracoServer]"); @@ -103,7 +103,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco { using (IScope scope = ScopeProvider.CreateScope()) { - scope.Database.BulkInsertRecords(servers); + ScopeAccessor.AmbientScope.Database.BulkInsertRecords(servers); scope.Complete(); } } @@ -111,7 +111,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco // Assert using (IScope scope = ScopeProvider.CreateScope()) { - Assert.That(scope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); + Assert.That(ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); } } @@ -136,7 +136,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco { using (IScope scope = ScopeProvider.CreateScope()) { - scope.Database.BulkInsertRecords(servers); + ScopeAccessor.AmbientScope.Database.BulkInsertRecords(servers); // Don't call complete here - the transaction will be rolled back. } @@ -145,7 +145,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco // Assert using (IScope scope = ScopeProvider.CreateScope()) { - Assert.That(scope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(0)); + Assert.That(ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(0)); } } @@ -168,7 +168,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco IDbCommand[] commands; using (IScope scope = ScopeProvider.CreateScope()) { - commands = scope.Database.GenerateBulkInsertCommands(servers.ToArray()); + commands = ScopeAccessor.AmbientScope.Database.GenerateBulkInsertCommands(servers.ToArray()); scope.Complete(); } @@ -198,7 +198,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco IDbCommand[] commands; using (IScope scope = ScopeProvider.CreateScope()) { - commands = scope.Database.GenerateBulkInsertCommands(servers.ToArray()); + commands = ScopeAccessor.AmbientScope.Database.GenerateBulkInsertCommands(servers.ToArray()); scope.Complete(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs index bea5a8fb8c..19976da976 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco { using (IScope scope = ScopeProvider.CreateScope()) { - InsertData(scope.Database); + InsertData(ScopeAccessor.AmbientScope.Database); scope.Complete(); } } @@ -166,11 +166,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco // var sql = @" // SELECT zbThing1.id, zbThing1.name // FROM zbThing1"; - Sql sql = scope.SqlContext.Sql() + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql() .Select() .From(); - List dtos = scope.Database.Fetch(sql); + List dtos = ScopeAccessor.AmbientScope.Database.Fetch(sql); Assert.AreEqual(2, dtos.Count); Assert.AreEqual("one", dtos.First(x => x.Id == 1).Name); } @@ -189,12 +189,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco // zbThing1.id Thing__id, zbThing1.name Thing__name // FROM zbThing2 // JOIN zbThing1 ON zbThing2.thingId=zbThing1.id"; - Sql sql = scope.SqlContext.Sql() + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql() .Select(r => r.Select(x => x.Thing)) .From() .InnerJoin().On((t2, t1) => t2.ThingId == t1.Id); - List dtos = scope.Database.Fetch(sql); + List dtos = ScopeAccessor.AmbientScope.Database.Fetch(sql); Assert.AreEqual(3, dtos.Count); Assert.AreEqual("uno", dtos.First(x => x.Id == 1).Name); Assert.IsNotNull(dtos.First(x => x.Id == 1).Thing); @@ -218,14 +218,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco // FROM zbThing1 // JOIN zbThing2 ON zbThing1.id=zbThing2.thingId // WHERE zbThing1.id=1"); - Sql sql = scope.SqlContext.Sql() + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql() .Select(r => r.Select(x => x.Things)) .From() .InnerJoin().On(left => left.Id, right => right.ThingId) .Where(x => x.Id == 1); // var dtos = scope.Database.FetchOneToMany(x => x.Things, x => x.Id, sql); - List dtos = scope.Database.FetchOneToMany(x => x.Things, sql); + List dtos = ScopeAccessor.AmbientScope.Database.FetchOneToMany(x => x.Things, sql); Assert.AreEqual(1, dtos.Count); Thing3Dto dto1 = dtos.FirstOrDefault(x => x.Id == 1); @@ -258,13 +258,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco // FROM zbThing1 // JOIN zbThing2 ON zbThing1.id=zbThing2.thingId // ORDER BY zbThing1.id"; - Sql sql = scope.SqlContext.Sql() + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql() .Select(r => r.Select(x => x.Things)) // select Thing3Dto, and Thing2Dto for Things .From() .InnerJoin().On(left => left.Id, right => right.ThingId) .OrderBy(x => x.Id); - List dtos = scope.Database.FetchOneToMany(x => x.Things, /*x => x.Id,*/ sql); + List dtos = ScopeAccessor.AmbientScope.Database.FetchOneToMany(x => x.Things, /*x => x.Id,*/ sql); Assert.AreEqual(2, dtos.Count); Thing3Dto dto1 = dtos.FirstOrDefault(x => x.Id == 1); @@ -283,18 +283,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco { using (IScope scope = ScopeProvider.CreateScope()) { - scope.SqlContext.Templates.Clear(); + ScopeAccessor.AmbientScope.SqlContext.Templates.Clear(); - Sql sql = scope.SqlContext.Templates.Get("xxx", s => s + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Templates.Get("xxx", s => s .Select(r => r.Select(x => x.Things)) // select Thing3Dto, and Thing2Dto for Things .From() .InnerJoin().On(left => left.Id, right => right.ThingId) .OrderBy(x => x.Id)).Sql(); // cached - sql = scope.SqlContext.Templates.Get("xxx", s => throw new InvalidOperationException()).Sql(); + sql = ScopeAccessor.AmbientScope.SqlContext.Templates.Get("xxx", s => throw new InvalidOperationException()).Sql(); - List dtos = scope.Database.FetchOneToMany(x => x.Things, /*x => x.Id,*/ sql); + List dtos = ScopeAccessor.AmbientScope.Database.FetchOneToMany(x => x.Things, /*x => x.Id,*/ sql); Assert.AreEqual(2, dtos.Count); Thing3Dto dto1 = dtos.FirstOrDefault(x => x.Id == 1); @@ -326,14 +326,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco // JOIN zbThing2Group ON zbThing1.id=zbThing2Group.thingId // JOIN zbThingGroup ON zbThing2Group.groupId=zbThingGroup.id // ORDER BY zbThing1.id"; - Sql sql = scope.SqlContext.Sql() + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql() .Select(r => r.Select(x => x.Groups)) .From() .InnerJoin().On((t, t2g) => t.Id == t2g.ThingId) .InnerJoin().On((t2g, tg) => t2g.GroupId == tg.Id) .OrderBy(x => x.Id); - List dtos = scope.Database.FetchOneToMany(x => x.Groups, /*x => x.Id,*/ sql); + List dtos = ScopeAccessor.AmbientScope.Database.FetchOneToMany(x => x.Groups, /*x => x.Id,*/ sql); Assert.AreEqual(2, dtos.Count); Thing4Dto dto1 = dtos.FirstOrDefault(x => x.Id == 1); @@ -360,14 +360,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco // FROM zbThing1 // JOIN zbThing2Group ON zbThing1.id=zbThing2Group.thingId // GROUP BY zbThing1.id, zbThing1.name"; - Sql sql = scope.SqlContext.Sql() + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql() .Select() .Append(", COUNT(zbThing2Group.groupId) AS groupCount") // FIXME: .From() .InnerJoin().On((t, t2g) => t.Id == t2g.ThingId) .GroupBy(x => x.Id, x => x.Name); - List dtos = scope.Database.Fetch(sql); + List dtos = ScopeAccessor.AmbientScope.Database.Fetch(sql); Assert.AreEqual(2, dtos.Count); Thing5Dto dto1 = dtos.FirstOrDefault(x => x.Id == 1); @@ -390,12 +390,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco { using (IScope scope = ScopeProvider.CreateScope()) { - Sql sql = scope.SqlContext.Sql() + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql() .SelectAll() .From() .Where(x => x.Id == 1); - Thing1Dto dto = scope.Database.Fetch(sql).FirstOrDefault(); + Thing1Dto dto = ScopeAccessor.AmbientScope.Database.Fetch(sql).FirstOrDefault(); Assert.IsNotNull(dto); Assert.AreEqual("one", dto.Name); @@ -406,7 +406,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco //// Assert.AreEqual("one", dto.Name); var sql3 = new Sql(sql.SQL, 1); - dto = scope.Database.Fetch(sql3).FirstOrDefault(); + dto = ScopeAccessor.AmbientScope.Database.Fetch(sql3).FirstOrDefault(); Assert.IsNotNull(dto); Assert.AreEqual("one", dto.Name); } @@ -418,28 +418,28 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco using (IScope scope = ScopeProvider.CreateScope()) { var tA1A = new ThingA1Dto { Id = 1, Name = "a1_a" }; - scope.Database.Insert(tA1A); + ScopeAccessor.AmbientScope.Database.Insert(tA1A); var tA1B = new ThingA1Dto { Id = 2, Name = "a1_b" }; - scope.Database.Insert(tA1B); + ScopeAccessor.AmbientScope.Database.Insert(tA1B); var tA1C = new ThingA1Dto { Id = 3, Name = "a1_c" }; - scope.Database.Insert(tA1C); + ScopeAccessor.AmbientScope.Database.Insert(tA1C); var tA2A = new ThingA2Dto { Id = 1, Name = "a2_a" }; - scope.Database.Insert(tA2A); + ScopeAccessor.AmbientScope.Database.Insert(tA2A); var tA2B = new ThingA2Dto { Id = 2, Name = "a2_b" }; - scope.Database.Insert(tA2B); + ScopeAccessor.AmbientScope.Database.Insert(tA2B); var tA2C = new ThingA2Dto { Id = 3, Name = "a2_c" }; - scope.Database.Insert(tA2C); + ScopeAccessor.AmbientScope.Database.Insert(tA2C); var tA3A = new ThingA3Dto { Id = 1, Name = "a3_a" }; - scope.Database.Insert(tA3A); + ScopeAccessor.AmbientScope.Database.Insert(tA3A); var tA3B = new ThingA3Dto { Id = 2, Name = "a3_b" }; - scope.Database.Insert(tA3B); + ScopeAccessor.AmbientScope.Database.Insert(tA3B); var k1 = new ThingA12Dto { Name = "a", Thing1Id = tA1A.Id, Thing2Id = tA2A.Id }; - scope.Database.Insert(k1); + ScopeAccessor.AmbientScope.Database.Insert(k1); var k2 = new ThingA12Dto { Name = "B", Thing1Id = tA1A.Id, Thing2Id = tA2B.Id }; - scope.Database.Insert(k2); + ScopeAccessor.AmbientScope.Database.Insert(k2); string sql = @"SELECT a1.id, a1.name, a2.id AS T2A__Id, a2.name AS T2A__Name, a3.id AS T2A__T3__Id, a3.name AS T2A__T3__Name, @@ -453,7 +453,7 @@ JOIN zbThingA2 a2x ON a12x.thing2id=a2x.id JOIN zbThingA3 a3x ON a2x.id=a3x.id "; - List ts = scope.Database.Fetch(sql); + List ts = ScopeAccessor.AmbientScope.Database.Fetch(sql); Assert.AreEqual(1, ts.Count); ThingA1Dto t = ts.First(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs index c586d496b7..4951146147 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -35,7 +36,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repo = new AuditRepository((IScopeAccessor)sp, _logger); repo.Save(new AuditItem(-1, AuditType.System, -1, UmbracoObjectTypes.Document.GetName(), "This is a System audit trail")); - List dtos = scope.Database.Fetch("WHERE id > -1"); + List dtos = ScopeAccessor.AmbientScope.Database.Fetch("WHERE id > -1"); Assert.That(dtos.Any(), Is.True); Assert.That(dtos.First().Comment, Is.EqualTo("This is a System audit trail")); @@ -63,7 +64,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { var repo = new AuditRepository((IScopeAccessor)sp, _logger); - IEnumerable page = repo.GetPagedResultsByQuery(sp.SqlContext.Query(), 0, 10, out long total, Direction.Descending, null, null); + IEnumerable page = repo.GetPagedResultsByQuery(sp.CreateQuery(), 0, 10, out long total, Direction.Descending, null, null); Assert.AreEqual(10, page.Count()); Assert.AreEqual(200, total); @@ -91,12 +92,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { var repo = new AuditRepository((IScopeAccessor)sp, _logger); - IQuery query = sp.SqlContext.Query().Where(x => x.UserId == -1); + IQuery query = sp.CreateQuery().Where(x => x.UserId == -1); try { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; IEnumerable page = repo.GetPagedResultsByQuery( query, @@ -105,7 +106,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos out long total, Direction.Descending, new[] { AuditType.Publish }, - sp.SqlContext.Query() + sp.CreateQuery() .Where(x => x.UserId > -2)); Assert.AreEqual(10, page.Count()); @@ -113,8 +114,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos } finally { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = false; } } } @@ -141,7 +142,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repo = new AuditRepository((IScopeAccessor)sp, _logger); IAuditItem[] page = repo.GetPagedResultsByQuery( - sp.SqlContext.Query(), + sp.CreateQuery(), 0, 9, out long total, @@ -178,13 +179,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repo = new AuditRepository((IScopeAccessor)sp, _logger); IAuditItem[] page = repo.GetPagedResultsByQuery( - sp.SqlContext.Query(), + sp.CreateQuery(), 0, 8, out long total, Direction.Descending, null, - sp.SqlContext.Query() + sp.CreateQuery() .Where(item => item.Comment == "Content created")) .ToArray(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/CacheInstructionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/CacheInstructionRepositoryTest.cs index faa5fd9d6e..f39b4311b2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/CacheInstructionRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/CacheInstructionRepositoryTest.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -98,7 +99,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repo = new CacheInstructionRepository((IScopeAccessor)sp); repo.Add(new CacheInstruction(0, date, Instructions, OriginIdentiy, InstructionCount)); - List dtos = scope.Database.Fetch("WHERE id > -1"); + List dtos = ScopeAccessor.AmbientScope.Database.Fetch("WHERE id > -1"); Assert.That(dtos.Any(), Is.True); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index 9f79414b55..38882c1638 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -17,6 +17,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Extensions; using Umbraco.Cms.Tests.Common.Testing; @@ -552,7 +553,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos ContentTypeRepository repository = ContentTypeRepository; // Act - IEnumerable contentTypes = repository.Get(scope.SqlContext.Query().Where(x => x.ParentId == contentType.Id)); + IEnumerable contentTypes = repository.Get(provider.CreateQuery().Where(x => x.ParentId == contentType.Id)); // Assert Assert.That(contentTypes.Count(), Is.EqualTo(3)); @@ -630,7 +631,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Act IEnumerable contentTypes = repository.GetMany(); int count = - scope.Database.ExecuteScalar( + ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM umbracoNode WHERE nodeObjectType = @NodeObjectType", new { NodeObjectType = Constants.ObjectTypes.DocumentType }); @@ -653,7 +654,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Act IEnumerable contentTypes = ((IReadRepository)repository).GetMany(allGuidIds); int count = - scope.Database.ExecuteScalar( + ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM umbracoNode WHERE nodeObjectType = @NodeObjectType", new { NodeObjectType = Constants.ObjectTypes.DocumentType }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs index baa94c6e9e..8cb7686982 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs @@ -279,7 +279,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos using (IScope scope = ScopeProvider.CreateScope()) { // Act - IQuery query = scope.SqlContext.Query().Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.RadioButtonList); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.RadioButtonList); IDataType[] result = DataTypeRepository.Get(query).ToArray(); // Assert @@ -295,7 +295,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos using (IScope scope = ScopeProvider.CreateScope()) { // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Name.StartsWith("D")); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Name.StartsWith("D")); int count = DataTypeRepository.Count(query); // Assert diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs index 87f33b8589..fc3caa6acf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -191,7 +191,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IDictionaryRepository repository = CreateRepository(); // Act - IQuery query = provider.SqlContext.Query().Where(x => x.ItemKey == "Article"); + IQuery query = provider.CreateQuery().Where(x => x.ItemKey == "Article"); IEnumerable result = repository.Get(query); // Assert @@ -211,7 +211,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IDictionaryRepository repository = CreateRepository(); // Act - IQuery query = provider.SqlContext.Query().Where(x => x.ItemKey.StartsWith("Read")); + IQuery query = provider.CreateQuery().Where(x => x.ItemKey.StartsWith("Read")); int result = repository.Count(query); // Assert diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index 7207718071..2059564cd8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -24,6 +24,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -152,11 +153,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos new IsolatedCaches(t => new ObjectCacheAppCache())); IScopeProvider provider = ScopeProvider; + IScopeAccessor scopeAccessor = ScopeAccessor; + using (IScope scope = provider.CreateScope()) { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out ContentTypeRepository contentTypeRepository, appCaches: realCache); - IUmbracoDatabase udb = scope.Database; + IUmbracoDatabase udb = scopeAccessor.AmbientScope.Database; udb.EnableSqlCount = false; @@ -230,7 +233,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(versions[^1], repository.Get(content1.Id).VersionId); // misc checks - Assert.AreEqual(true, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); + Assert.AreEqual(true, ScopeAccessor.AmbientScope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // change something // save = update the current (draft) version @@ -246,7 +249,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(versions[^1], repository.Get(content1.Id).VersionId); // misc checks - Assert.AreEqual(true, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); + Assert.AreEqual(true, ScopeAccessor.AmbientScope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // unpublish = no impact on versions ((Content)content1).PublishedState = PublishedState.Unpublishing; @@ -261,7 +264,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(versions[^1], repository.Get(content1.Id).VersionId); // misc checks - Assert.AreEqual(false, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); + Assert.AreEqual(false, ScopeAccessor.AmbientScope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // change something // save = update the current (draft) version @@ -276,7 +279,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(versions[^1], repository.Get(content1.Id).VersionId); // misc checks - Assert.AreEqual(false, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); + Assert.AreEqual(false, ScopeAccessor.AmbientScope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // publish = version content1.PublishCulture(CultureImpact.Invariant); @@ -292,7 +295,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(versions[^1], repository.Get(content1.Id).VersionId); // misc checks - Assert.AreEqual(true, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); + Assert.AreEqual(true, ScopeAccessor.AmbientScope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // change something // save = update the current (draft) version @@ -310,7 +313,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(versions[^1], repository.Get(content1.Id).VersionId); // misc checks - Assert.AreEqual(true, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); + Assert.AreEqual(true, ScopeAccessor.AmbientScope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // publish = new version content1.Name = "name-4"; @@ -328,7 +331,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(versions[^1], repository.Get(content1.Id).VersionId); // misc checks - Assert.AreEqual(true, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); + Assert.AreEqual(true, ScopeAccessor.AmbientScope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // all versions IContent[] allVersions = repository.GetAllVersions(content1.Id).ToArray(); @@ -700,7 +703,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.Get(query); Assert.GreaterOrEqual(2, result.Count()); @@ -838,10 +841,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos try { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; - IQuery query = scope.SqlContext.Query().Where(x => x.ParentId == root.Id); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.ParentId == root.Id); IEnumerable result = repository.GetPage(query, 0, 20, out long totalRecords, null, Ordering.By("UpdateDate")); Assert.AreEqual(25, totalRecords); @@ -864,8 +867,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos } finally { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = false; } } } @@ -878,12 +881,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Name.Contains("Text")); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Name.Contains("Text")); try { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; IEnumerable result = repository.GetPage(query, 0, 2, out long totalRecords, null, Ordering.By("title", isCustomField: true)); @@ -896,8 +899,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos } finally { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = false; } } } @@ -910,12 +913,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); try { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, null, Ordering.By("Name")); @@ -925,8 +928,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos } finally { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = false; } } } @@ -939,7 +942,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.GetPage(query, 1, 1, out long totalRecords, null, Ordering.By("Name")); Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -956,7 +959,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.GetPage(query, 0, 2, out long totalRecords, null, Ordering.By("Name")); Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -973,7 +976,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, null, Ordering.By("Name", Direction.Descending)); Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -990,9 +993,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); - IQuery filterQuery = scope.SqlContext.Query().Where(x => x.Name.Contains("Page 2")); + IQuery filterQuery = ScopeProvider.CreateQuery().Where(x => x.Name.Contains("Page 2")); IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, filterQuery, Ordering.By("Name")); Assert.That(totalRecords, Is.EqualTo(1)); @@ -1009,9 +1012,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); - IQuery filterQuery = scope.SqlContext.Query().Where(x => x.Name.Contains("text")); + IQuery filterQuery = ScopeProvider.CreateQuery().Where(x => x.Name.Contains("text")); IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, filterQuery, Ordering.By("Name")); Assert.That(totalRecords, Is.EqualTo(2)); @@ -1084,7 +1087,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); int result = repository.Count(query); Assert.That(result, Is.GreaterThanOrEqualTo(2)); @@ -1099,7 +1102,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { DocumentRepository repository = CreateRepository((IScopeAccessor)provider, out _); - IQuery query = scope.SqlContext.Query().Where(x => x.Key == new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Key == new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); IContent content = repository.Get(query).SingleOrDefault(); Assert.IsNotNull(content); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs index ba5c176ee2..c25bd69653 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -78,7 +79,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos using (var scope = ScopeProvider.CreateScope()) { - scope.Database.Update("set preventCleanup = 1 where id in (1,3)"); + ScopeAccessor.AmbientScope.Database.Update("set preventCleanup = 1 where id in (1,3)"); var sut = new DocumentVersionRepository(ScopeAccessor); var results = sut.GetDocumentVersionsEligibleForCleanup(); @@ -113,7 +114,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos ContentService.SaveAndPublish(content); using (var scope = ScopeProvider.CreateScope()) { - var query = scope.SqlContext.Sql(); + var query = ScopeAccessor.AmbientScope.SqlContext.Sql(); query.Select() .From(); @@ -121,7 +122,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var sut = new DocumentVersionRepository(ScopeAccessor); sut.DeleteVersions(new []{1,2,3}); - var after = scope.Database.Fetch(query); + var after = ScopeAccessor.AmbientScope.Database.Fetch(query); Assert.Multiple(() => { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DomainRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DomainRepositoryTest.cs index c53099cee9..4928354d97 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DomainRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DomainRepositoryTest.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/EntityRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/EntityRepositoryTest.cs index 9da3f6ef59..a70355c887 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/EntityRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/EntityRepositoryTest.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -68,7 +69,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos System.Guid[] objectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member }; - IQuery query = provider.SqlContext.Query() + IQuery query = provider.CreateQuery() .WhereIn(e => e.Id, ids); var entities = repo.GetPagedResultsByQuery(query, objectTypes, 0, 20, out long totalRecords, null, null).ToList(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/KeyValueRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/KeyValueRepositoryTests.cs index 3f3a709ce9..4f6934a481 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/KeyValueRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/KeyValueRepositoryTests.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/LanguageRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/LanguageRepositoryTest.cs index 242533c651..3c157d9c5a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/LanguageRepositoryTest.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -40,7 +41,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IScopeProvider provider = ScopeProvider; using (IScope scope = provider.CreateScope()) { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; LanguageRepository repository = CreateRepository(provider); // Act @@ -150,7 +151,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos LanguageRepository repository = CreateRepository(provider); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.IsoCode == "da-DK"); + IQuery query = provider.CreateQuery().Where(x => x.IsoCode == "da-DK"); IEnumerable result = repository.Get(query); // Assert @@ -170,7 +171,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos LanguageRepository repository = CreateRepository(provider); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.IsoCode.StartsWith("D")); + IQuery query = provider.CreateQuery().Where(x => x.IsoCode.StartsWith("D")); int count = repository.Count(query); // Assert 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 2766e66426..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; @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -127,7 +128,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repository = new MacroRepository((IScopeAccessor)provider, AppCaches.Disabled, _logger, ShortStringHelper); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Alias.ToUpper() == "TEST1"); + IQuery query = ScopeAccessor.AmbientScope.SqlContext.Query().Where(x => x.Alias.ToUpper() == "TEST1"); IEnumerable result = repository.Get((IQuery)query); // Assert @@ -145,7 +146,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repository = new MacroRepository((IScopeAccessor)provider, AppCaches.Disabled, _logger, ShortStringHelper); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Name.StartsWith("Test")); + IQuery query = ScopeAccessor.AmbientScope.SqlContext.Query().Where(x => x.Name.StartsWith("Test")); int count = repository.Count(query); // Assert diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs index fc98b8ef10..1acba24035 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs @@ -24,6 +24,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -84,7 +85,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { MediaRepository repository = CreateRepository(provider, out MediaTypeRepository mediaTypeRepository, appCaches: realCache); - IUmbracoDatabase udb = scope.Database; + IUmbracoDatabase udb = ScopeAccessor.AmbientScope.Database; udb.EnableSqlCount = false; @@ -271,7 +272,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos MediaRepository repository = CreateRepository(provider, out MediaTypeRepository mediaTypeRepository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = provider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.Get(query); // Assert @@ -297,7 +298,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos } int[] types = new[] { 1031 }; - IQuery query = scope.SqlContext.Query().Where(x => types.Contains(x.ContentTypeId)); + IQuery query = provider.CreateQuery().Where(x => types.Contains(x.ContentTypeId)); IEnumerable result = repository.Get(query); // Assert @@ -327,7 +328,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos } string[] types = new[] { "Folder" }; - IQuery query = scope.SqlContext.Query().Where(x => types.Contains(x.ContentType.Alias)); + IQuery query = provider.CreateQuery().Where(x => types.Contains(x.ContentType.Alias)); IEnumerable result = repository.Get(query); // Assert @@ -345,7 +346,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos MediaRepository repository = CreateRepository(provider, out MediaTypeRepository mediaTypeRepository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = provider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, null, Ordering.By("SortOrder")); // Assert @@ -365,7 +366,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos MediaRepository repository = CreateRepository(provider, out MediaTypeRepository mediaTypeRepository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = provider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.GetPage(query, 1, 1, out long totalRecords, null, Ordering.By("SortOrder")); // Assert @@ -385,7 +386,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos MediaRepository repository = CreateRepository(provider, out MediaTypeRepository mediaTypeRepository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = provider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.GetPage(query, 0, 2, out long totalRecords, null, Ordering.By("SortOrder")); // Assert @@ -405,7 +406,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos MediaRepository repository = CreateRepository(provider, out MediaTypeRepository mediaTypeRepository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = provider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, null, Ordering.By("SortOrder", Direction.Descending)); // Assert @@ -425,7 +426,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos MediaRepository repository = CreateRepository(provider, out MediaTypeRepository mediaTypeRepository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = provider.CreateQuery().Where(x => x.Level == 2); IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, null, Ordering.By("Name")); // Assert @@ -445,9 +446,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos MediaRepository repository = CreateRepository(provider, out MediaTypeRepository mediaTypeRepository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = provider.CreateQuery().Where(x => x.Level == 2); - IQuery filter = scope.SqlContext.Query().Where(x => x.Name.Contains("File")); + IQuery filter = provider.CreateQuery().Where(x => x.Name.Contains("File")); IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, filter, Ordering.By("SortOrder")); // Assert @@ -467,9 +468,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos MediaRepository repository = CreateRepository(provider, out _); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Level == 2); + IQuery query = provider.CreateQuery().Where(x => x.Level == 2); - IQuery filter = scope.SqlContext.Query().Where(x => x.Name.Contains("Test")); + IQuery filter = provider.CreateQuery().Where(x => x.Name.Contains("Test")); IEnumerable result = repository.GetPage(query, 0, 1, out long totalRecords, filter, Ordering.By("SortOrder")); // Assert @@ -559,7 +560,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Act int level = 2; - IQuery query = scope.SqlContext.Query().Where(x => x.Level == level); + IQuery query = provider.CreateQuery().Where(x => x.Level == level); int result = repository.Count(query); // Assert diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaTypeRepositoryTest.cs index 46cdca4194..51a71c30e1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -294,7 +295,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Act IEnumerable mediaTypes = repository.GetMany(); int count = - scope.Database.ExecuteScalar( + ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM umbracoNode WHERE nodeObjectType = @NodeObjectType", new { NodeObjectType = Constants.ObjectTypes.MediaType }); @@ -319,7 +320,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IEnumerable mediaTypes = ((IReadRepository)repository).GetMany(allGuidIds); int count = - scope.Database.ExecuteScalar( + ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM umbracoNode WHERE nodeObjectType = @NodeObjectType", new { NodeObjectType = Constants.ObjectTypes.MediaType }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs index d8bf4be14a..d5fb7d3edc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs @@ -28,6 +28,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -151,7 +152,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IMember member = CreateTestMember(key: key); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Key == key); + IQuery query = provider.CreateQuery().Where(x => x.Key == key); IEnumerable result = repository.Get(query); // Assert @@ -271,20 +272,23 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { IScopeProvider provider = ScopeProvider; - IQuery query = provider.SqlContext.Query().Where(x => - ((Member)x).LongStringPropertyValue.Contains("1095") && - ((Member)x).PropertyTypeAlias == "headshot"); + using (provider.CreateScope()) + { + IQuery query = provider.CreateQuery().Where(x => + ((Member)x).LongStringPropertyValue.Contains("1095") && + ((Member)x).PropertyTypeAlias == "headshot"); - Sql sqlSubquery = GetSubquery(); - var translator = new SqlTranslator(sqlSubquery, query); - Sql subquery = translator.Translate(); - Sql sql = GetBaseQuery(false) - .Append("WHERE umbracoNode.id IN (" + subquery.SQL + ")", subquery.Arguments) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); + Sql sqlSubquery = GetSubquery(); + var translator = new SqlTranslator(sqlSubquery, query); + Sql subquery = translator.Translate(); + Sql sql = GetBaseQuery(false) + .Append("WHERE umbracoNode.id IN (" + subquery.SQL + ")", subquery.Arguments) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); - Debug.Print(sql.SQL); - Assert.That(sql.SQL, Is.Not.Empty); + Debug.Print(sql.SQL); + Assert.That(sql.SQL, Is.Not.Empty); + } } private IMember CreateTestMember(IMemberType memberType = null, string name = null, string email = null, string password = null, string username = null, Guid? key = null) @@ -327,7 +331,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IScopeProvider provider = ScopeProvider; if (isCount) { - Sql sqlCount = provider.SqlContext.Sql() + Sql sqlCount = ScopeAccessor.AmbientScope.SqlContext.Sql() .SelectCount() .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) @@ -338,7 +342,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos return sqlCount; } - Sql sql = provider.SqlContext.Sql(); + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql(); sql.Select( "umbracoNode.*", $"{Constants.DatabaseSchema.Tables.Content}.contentTypeId", @@ -381,7 +385,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos private Sql GetSubquery() { IScopeProvider provider = ScopeProvider; - Sql sql = provider.SqlContext.Sql(); + Sql sql = ScopeAccessor.AmbientScope.SqlContext.Sql(); sql.Select("umbracoNode.id") .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberTypeRepositoryTest.cs index c963375e3f..33209abc6a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -14,6 +14,7 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/NotificationsRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/NotificationsRepositoryTest.cs index be625e4afd..f7bfbb989e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/NotificationsRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/NotificationsRepositoryTest.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; using Constants = Umbraco.Cms.Core.Constants; @@ -44,7 +45,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos UniqueId = Guid.NewGuid(), UserId = Constants.Security.SuperUserId }; - object result = scope.Database.Insert(node); + object result = ScopeAccessor.AmbientScope.Database.Insert(node); IEntity entity = Mock.Of(e => e.Id == node.NodeId); IUser user = Mock.Of(e => e.Id == node.UserId); @@ -66,7 +67,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repo = new NotificationsRepository((IScopeAccessor)provider); var userDto = new UserDto { Email = "test", Login = "test", Password = "test", UserName = "test", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; - scope.Database.Insert(userDto); + ScopeAccessor.AmbientScope.Database.Insert(userDto); IUser userNew = Mock.Of(e => e.Id == userDto.Id); IUser userAdmin = Mock.Of(e => e.Id == Constants.Security.SuperUserId); @@ -74,7 +75,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos for (int i = 0; i < 10; i++) { var node = new NodeDto { CreateDate = DateTime.Now, Level = 1, NodeObjectType = Constants.ObjectTypes.ContentItem, ParentId = -1, Path = "-1," + i, SortOrder = 1, Text = "hello" + i, Trashed = false, UniqueId = Guid.NewGuid(), UserId = -1 }; - object result = scope.Database.Insert(node); + object result = ScopeAccessor.AmbientScope.Database.Insert(node); IEntity entity = Mock.Of(e => e.Id == node.NodeId); Notification notification = repo.CreateNotification((i % 2 == 0) ? userAdmin : userNew, entity, i.ToString(CultureInfo.InvariantCulture)); } @@ -94,16 +95,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repo = new NotificationsRepository((IScopeAccessor)provider); var node1 = new NodeDto { CreateDate = DateTime.Now, Level = 1, NodeObjectType = Constants.ObjectTypes.ContentItem, ParentId = -1, Path = "-1,1", SortOrder = 1, Text = "hello1", Trashed = false, UniqueId = Guid.NewGuid(), UserId = -1 }; - scope.Database.Insert(node1); + ScopeAccessor.AmbientScope.Database.Insert(node1); IEntity entity1 = Mock.Of(e => e.Id == node1.NodeId); var node2 = new NodeDto { CreateDate = DateTime.Now, Level = 1, NodeObjectType = Constants.ObjectTypes.ContentItem, ParentId = -1, Path = "-1,2", SortOrder = 1, Text = "hello2", Trashed = false, UniqueId = Guid.NewGuid(), UserId = -1 }; - scope.Database.Insert(node2); + ScopeAccessor.AmbientScope.Database.Insert(node2); IEntity entity2 = Mock.Of(e => e.Id == node2.NodeId); for (int i = 0; i < 10; i++) { var userDto = new UserDto { Email = "test" + i, Login = "test" + i, Password = "test", UserName = "test" + i, UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; - scope.Database.Insert(userDto); + ScopeAccessor.AmbientScope.Database.Insert(userDto); IUser userNew = Mock.Of(e => e.Id == userDto.Id); Notification notification = repo.CreateNotification(userNew, (i % 2 == 0) ? entity1 : entity2, i.ToString(CultureInfo.InvariantCulture)); } @@ -123,16 +124,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repo = new NotificationsRepository((IScopeAccessor)provider); var node1 = new NodeDto { CreateDate = DateTime.Now, Level = 1, NodeObjectType = Constants.ObjectTypes.ContentItem, ParentId = -1, Path = "-1,1", SortOrder = 1, Text = "hello1", Trashed = false, UniqueId = Guid.NewGuid(), UserId = -1 }; - scope.Database.Insert(node1); + ScopeAccessor.AmbientScope.Database.Insert(node1); IEntity entity1 = Mock.Of(e => e.Id == node1.NodeId); var node2 = new NodeDto { CreateDate = DateTime.Now, Level = 1, NodeObjectType = Constants.ObjectTypes.ContentItem, ParentId = -1, Path = "-1,2", SortOrder = 1, Text = "hello2", Trashed = false, UniqueId = Guid.NewGuid(), UserId = -1 }; - scope.Database.Insert(node2); + ScopeAccessor.AmbientScope.Database.Insert(node2); IEntity entity2 = Mock.Of(e => e.Id == node2.NodeId); for (int i = 0; i < 10; i++) { var userDto = new UserDto { Email = "test" + i, Login = "test" + i, Password = "test", UserName = "test" + i, UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; - scope.Database.Insert(userDto); + ScopeAccessor.AmbientScope.Database.Insert(userDto); IUser userNew = Mock.Of(e => e.Id == userDto.Id); Notification notification = repo.CreateNotification(userNew, (i % 2 == 0) ? entity1 : entity2, i.ToString(CultureInfo.InvariantCulture)); } @@ -152,7 +153,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var repo = new NotificationsRepository((IScopeAccessor)provider); var userDto = new UserDto { Email = "test", Login = "test", Password = "test", UserName = "test", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; - scope.Database.Insert(userDto); + ScopeAccessor.AmbientScope.Database.Insert(userDto); IUser userNew = Mock.Of(e => e.Id == userDto.Id); IUser userAdmin = Mock.Of(e => e.Id == Constants.Security.SuperUserId); @@ -160,7 +161,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos for (int i = 0; i < 10; i++) { var node = new NodeDto { CreateDate = DateTime.Now, Level = 1, NodeObjectType = Constants.ObjectTypes.ContentItem, ParentId = -1, Path = "-1," + i, SortOrder = 1, Text = "hello" + i, Trashed = false, UniqueId = Guid.NewGuid(), UserId = -1 }; - object result = scope.Database.Insert(node); + object result = ScopeAccessor.AmbientScope.Database.Insert(node); IEntity entity = Mock.Of(e => e.Id == node.NodeId); Notification notification = repo.CreateNotification((i % 2 == 0) ? userAdmin : userNew, entity, i.ToString(CultureInfo.InvariantCulture)); } 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/PublicAccessRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PublicAccessRepositoryTest.cs index 3926c4975c..283c8e57aa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -62,7 +63,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IScopeProvider provider = ScopeProvider; using (IScope scope = provider.CreateScope()) { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); PublicAccessRule[] rules = new[] @@ -102,7 +103,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IScopeProvider provider = ScopeProvider; using (IScope scope = provider.CreateScope()) { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); PublicAccessRule[] rules = new[] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RedirectUrlRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RedirectUrlRepositoryTests.cs index 04a0fd3ebb..af75016d9a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RedirectUrlRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RedirectUrlRepositoryTests.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs index ee01816f09..a20706c597 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -361,7 +362,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos RelationRepository repository = CreateRepository(ScopeProvider, out RelationTypeRepository repositoryType); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.ParentId == _textpage.Id); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.ParentId == _textpage.Id); int count = repository.Count(query); // Assert @@ -378,7 +379,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos RelationRepository repository = CreateRepository(ScopeProvider, out RelationTypeRepository repositoryType); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.RelationTypeId == _relateContent.Id); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.RelationTypeId == _relateContent.Id); IEnumerable relations = repository.Get(query); // Assert diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs index 4b0a23464f..d99de65721 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; using Constants = Umbraco.Cms.Core.Constants; @@ -183,7 +184,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos RelationTypeRepository repository = CreateRepository(provider); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Alias.StartsWith("relate")); + IQuery query = provider.CreateQuery().Where(x => x.Alias.StartsWith("relate")); int count = repository.Count(query); // Assert @@ -202,7 +203,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Act System.Guid childObjType = Constants.ObjectTypes.DocumentType; - IQuery query = scope.SqlContext.Query().Where(x => x.ChildObjectType == childObjType); + IQuery query = provider.CreateQuery().Where(x => x.ChildObjectType == childObjType); IEnumerable result = repository.Get(query); // Assert 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 f85796f622..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,14 +3,15 @@ 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; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; 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/Persistence/Repositories/TagRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs index e2972ad504..8dc0a2ed81 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -864,7 +865,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos DocumentRepository.Delete(content1); - Assert.AreEqual(0, scope.Database.ExecuteScalar( + Assert.AreEqual(0, ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", new { nodeId = content1.Id, propTypeId = contentType.PropertyTypes.First().Id })); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index 80eb61d21f..7b0b8b9e21 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -22,6 +22,7 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserGroupRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserGroupRepositoryTest.cs index b38814b928..367d4f770e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserGroupRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserGroupRepositoryTest.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -180,7 +181,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos CreateAndCommitMultipleUserGroups(repository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Alias == "testGroup1"); + IQuery query = provider.CreateQuery().Where(x => x.Alias == "testGroup1"); IEnumerable result = repository.Get(query); // Assert @@ -261,7 +262,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IUserGroup[] userGroups = CreateAndCommitMultipleUserGroups(repository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Alias == "testGroup1" || x.Alias == "testGroup2"); + IQuery query = provider.CreateQuery().Where(x => x.Alias == "testGroup1" || x.Alias == "testGroup2"); int result = repository.Count(query); // Assert diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs index a613a42c36..52af0ac615 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs @@ -21,6 +21,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -192,7 +193,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos CreateAndCommitMultipleUsers(repository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1"); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Username == "TestUser1"); IEnumerable result = repository.Get(query); // Assert @@ -273,7 +274,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IUser[] users = CreateAndCommitMultipleUsers(repository); // Act - IQuery query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + IQuery query = ScopeProvider.CreateQuery().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); int result = repository.Count(query); // Assert @@ -290,12 +291,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos UserRepository repository = CreateRepository(provider); IUser[] users = CreateAndCommitMultipleUsers(repository); - IQuery query = provider.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + IQuery query = provider.CreateQuery().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); try { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; // Act IEnumerable result = repository.GetPagedResultsByQuery( @@ -306,15 +307,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos user => user.Id, Direction.Ascending, excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, - filter: provider.SqlContext.Query().Where(x => x.Id > -1)); + filter: provider.CreateQuery().Where(x => x.Id > -1)); // Assert Assert.AreEqual(2, totalRecs); } finally { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = false; } } } @@ -331,8 +332,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos try { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; // Act IEnumerable result = repository.GetPagedResultsByQuery( @@ -344,15 +345,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Direction.Ascending, includeUserGroups: new[] { Constants.Security.AdminGroupAlias, Constants.Security.SensitiveDataGroupAlias }, excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, - filter: provider.SqlContext.Query().Where(x => x.Id == -1)); + filter: provider.CreateQuery().Where(x => x.Id == -1)); // Assert Assert.AreEqual(1, totalRecs); } finally { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = false; } } } @@ -404,7 +405,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Guid sessionId = repository.CreateLoginSession(user.Id, "1.2.3.4"); // manually update this record to be in the past - scope.Database.Execute(scope.SqlContext.Sql() + ScopeAccessor.AmbientScope.Database.Execute(ScopeAccessor.AmbientScope.SqlContext.Sql() .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow.AddDays(-100))) .Where(x => x.SessionId == sessionId)); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs index 13f543c130..90f6fab9e1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Events; @@ -22,7 +22,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence using (var scope = ScopeProvider.CreateScope()) { - var schema = new DatabaseSchemaCreator(scope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); + var schema = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); result = schema.ValidateSchema(DatabaseSchemaCreator.OrderedTables); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs index 4b3d159ff5..8c34202fe4 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -36,7 +36,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -50,7 +50,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -65,7 +65,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -79,7 +79,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -94,7 +94,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -108,7 +108,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -124,7 +124,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -138,7 +138,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -151,7 +151,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -166,7 +166,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -180,7 +180,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -197,7 +197,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -213,7 +213,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -227,7 +227,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -240,7 +240,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -253,7 +253,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -266,7 +266,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -279,7 +279,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -295,7 +295,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -312,7 +312,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -327,7 +327,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -345,7 +345,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -362,7 +362,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -377,7 +377,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -392,7 +392,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -405,7 +405,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -418,7 +418,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -439,7 +439,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); @@ -452,7 +452,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -466,7 +466,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -480,7 +480,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); @@ -495,7 +495,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); + var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); helper.CreateTable(); helper.CreateTable(); 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/Scoping/ScopeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs index 37a21cf9b4..c4f2db58fe 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Castle.Core.Logging; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -16,6 +17,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping { @@ -28,16 +30,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping [SetUp] public void SetUp() => Assert.IsNull(ScopeProvider.AmbientScope); // gone - protected override AppCaches GetAppCaches() + + protected override void ConfigureTestServices(IServiceCollection services) { // Need to have a mockable request cache for tests var appCaches = new AppCaches( NoAppCache.Instance, Mock.Of(x => x.IsAvailable == false), new IsolatedCaches(_ => NoAppCache.Instance)); - return appCaches; + + services.AddUnique(appCaches); } + [Test] public void GivenUncompletedScopeOnChildThread_WhenTheParentCompletes_TheTransactionIsRolledBack() { @@ -187,7 +192,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping Assert.IsInstanceOf(scope); Assert.IsNotNull(scopeProvider.AmbientScope); Assert.AreSame(scope, scopeProvider.AmbientScope); - database = scope.Database; // populates scope's database + database = ScopeAccessor.AmbientScope.Database; // populates scope's database Assert.IsNotNull(database); Assert.IsNotNull(database.Connection); // in a transaction } @@ -360,7 +365,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping Assert.IsInstanceOf(scope); Assert.IsNotNull(scopeProvider.AmbientScope); Assert.AreSame(scope, scopeProvider.AmbientScope); - database = scope.Database; // populates scope's database + database = ScopeAccessor.AmbientScope.Database; // populates scope's database Assert.IsNotNull(database); Assert.IsNotNull(database.Connection); // in a transaction using (IScope nested = scopeProvider.CreateScope()) @@ -369,7 +374,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping Assert.IsNotNull(scopeProvider.AmbientScope); Assert.AreSame(nested, scopeProvider.AmbientScope); Assert.AreSame(scope, ((Scope)nested).ParentScope); - Assert.AreSame(database, nested.Database); + Assert.AreSame(database, ScopeAccessor.AmbientScope.Database); } Assert.IsNotNull(database.Connection); // still @@ -386,32 +391,32 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute("CREATE TABLE tmp3 (id INT, name NVARCHAR(64))"); + ScopeAccessor.AmbientScope.Database.Execute("CREATE TABLE tmp3 (id INT, name NVARCHAR(64))"); scope.Complete(); } using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute("INSERT INTO tmp3 (id, name) VALUES (1, 'a')"); - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp3 WHERE id=1"); + ScopeAccessor.AmbientScope.Database.Execute("INSERT INTO tmp3 (id, name) VALUES (1, 'a')"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp3 WHERE id=1"); Assert.AreEqual("a", n); } using (IScope scope = scopeProvider.CreateScope()) { - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp3 WHERE id=1"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp3 WHERE id=1"); Assert.IsNull(n); } using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute("INSERT INTO tmp3 (id, name) VALUES (1, 'a')"); + ScopeAccessor.AmbientScope.Database.Execute("INSERT INTO tmp3 (id, name) VALUES (1, 'a')"); scope.Complete(); } using (IScope scope = scopeProvider.CreateScope()) { - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp3 WHERE id=1"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp3 WHERE id=1"); Assert.AreEqual("a", n); } } @@ -423,24 +428,24 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute($"CREATE TABLE tmp1 (id INT, name NVARCHAR(64))"); + ScopeAccessor.AmbientScope.Database.Execute($"CREATE TABLE tmp1 (id INT, name NVARCHAR(64))"); scope.Complete(); } using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute("INSERT INTO tmp1 (id, name) VALUES (1, 'a')"); - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=1"); + ScopeAccessor.AmbientScope.Database.Execute("INSERT INTO tmp1 (id, name) VALUES (1, 'a')"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=1"); Assert.AreEqual("a", n); using (IScope nested = scopeProvider.CreateScope()) { - nested.Database.Execute("INSERT INTO tmp1 (id, name) VALUES (2, 'b')"); - string nn = nested.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=2"); + ScopeAccessor.AmbientScope.Database.Execute("INSERT INTO tmp1 (id, name) VALUES (2, 'b')"); + string nn = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=2"); Assert.AreEqual("b", nn); } - n = scope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=2"); + n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=2"); Assert.AreEqual("b", n); scope.Complete(); @@ -448,9 +453,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping using (IScope scope = scopeProvider.CreateScope()) { - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=1"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=1"); Assert.IsNull(n); - n = scope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=2"); + n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp1 WHERE id=2"); Assert.IsNull(n); } } @@ -462,33 +467,33 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute("CREATE TABLE tmp2 (id INT, name NVARCHAR(64))"); + ScopeAccessor.AmbientScope.Database.Execute("CREATE TABLE tmp2 (id INT, name NVARCHAR(64))"); scope.Complete(); } using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute("INSERT INTO tmp2 (id, name) VALUES (1, 'a')"); - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=1"); + ScopeAccessor.AmbientScope.Database.Execute("INSERT INTO tmp2 (id, name) VALUES (1, 'a')"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=1"); Assert.AreEqual("a", n); using (IScope nested = scopeProvider.CreateScope()) { - nested.Database.Execute("INSERT INTO tmp2 (id, name) VALUES (2, 'b')"); - string nn = nested.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=2"); + ScopeAccessor.AmbientScope.Database.Execute("INSERT INTO tmp2 (id, name) VALUES (2, 'b')"); + string nn = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=2"); Assert.AreEqual("b", nn); nested.Complete(); } - n = scope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=2"); + n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=2"); Assert.AreEqual("b", n); } using (IScope scope = scopeProvider.CreateScope()) { - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=1"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=1"); Assert.IsNull(n); - n = scope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=2"); + n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp2 WHERE id=2"); Assert.IsNull(n); } } @@ -500,34 +505,34 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute("CREATE TABLE tmp (id INT, name NVARCHAR(64))"); + ScopeAccessor.AmbientScope.Database.Execute("CREATE TABLE tmp (id INT, name NVARCHAR(64))"); scope.Complete(); } using (IScope scope = scopeProvider.CreateScope()) { - scope.Database.Execute("INSERT INTO tmp (id, name) VALUES (1, 'a')"); - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=1"); + ScopeAccessor.AmbientScope.Database.Execute("INSERT INTO tmp (id, name) VALUES (1, 'a')"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=1"); Assert.AreEqual("a", n); using (IScope nested = scopeProvider.CreateScope()) { - nested.Database.Execute("INSERT INTO tmp (id, name) VALUES (2, 'b')"); - string nn = nested.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=2"); + ScopeAccessor.AmbientScope.Database.Execute("INSERT INTO tmp (id, name) VALUES (2, 'b')"); + string nn = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=2"); Assert.AreEqual("b", nn); nested.Complete(); } - n = scope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=2"); + n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=2"); Assert.AreEqual("b", n); scope.Complete(); } using (IScope scope = scopeProvider.CreateScope()) { - string n = scope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=1"); + string n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=1"); Assert.AreEqual("a", n); - n = scope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=2"); + n = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT name FROM tmp WHERE id=2"); Assert.AreEqual("b", n); } } @@ -615,13 +620,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping public void ScopeReference() { ScopeProvider scopeProvider = ScopeProvider; - IScope scope = scopeProvider.CreateScope(); - IScope nested = scopeProvider.CreateScope(); + Scope scope = (Scope) scopeProvider.CreateScope(); + Scope nested = (Scope) scopeProvider.CreateScope(); + Assert.IsNotNull(scopeProvider.AmbientScope); + var scopeRef = new HttpScopeReference(scopeProvider); scopeRef.Register(); scopeRef.Dispose(); + Assert.IsNull(scopeProvider.AmbientScope); + Assert.Throws(() => { IUmbracoDatabase db = scope.Database; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index b05257712c..c228e07178 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -31,15 +31,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping private ILocalizationService LocalizationService => GetRequiredService(); - protected override AppCaches GetAppCaches() + protected override void ConfigureTestServices(IServiceCollection services) { // this is what's created core web runtime - var result = new AppCaches( + var appCaches = new AppCaches( new DeepCloneAppCache(new ObjectCacheAppCache()), NoAppCache.Instance, new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); - return result; + services.AddUnique(appCaches); } protected override void CustomTestSetup(IUmbracoBuilder builder) @@ -318,20 +318,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping public static string GetCacheIdKey(object id) => $"{GetCacheTypeKey()}{id}"; public static string GetCacheTypeKey() => $"uRepo_{typeof(T).Name}_"; - - public class PassiveEventDispatcher : QueuingEventDispatcherBase - { - public PassiveEventDispatcher() - : base(false) - { - } - - protected override void ScopeExitCompleted() - { - // do nothing - } - } - + public class LocalServerMessenger : ServerMessengerBase { public LocalServerMessenger() 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/CacheInstructionServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs index bce214eada..80005f8058 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs @@ -142,7 +142,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services .From(); using (IScope scope = ScopeProvider.CreateScope()) { - cacheInstructions = scope.Database.Fetch(sql); + cacheInstructions = ScopeAccessor.AmbientScope.Database.Fetch(sql); scope.Complete(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs index 32883138c2..c082d93946 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs @@ -624,15 +624,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services using (IScope scope = ScopeProvider.CreateScope()) { - Assert.AreEqual(4, scope.Database.ExecuteScalar( + Assert.AreEqual(4, ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", new { nodeId = content.Id, propTypeId = propertyTypeId })); - Assert.AreEqual(3, scope.Database.ExecuteScalar( + Assert.AreEqual(3, ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", new { nodeId = child1.Id, propTypeId = propertyTypeId })); - Assert.AreEqual(2, scope.Database.ExecuteScalar( + Assert.AreEqual(2, ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", new { nodeId = child2.Id, propTypeId = propertyTypeId })); @@ -667,7 +667,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services int propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; using (IScope scope = ScopeProvider.CreateScope()) { - Assert.AreEqual(4, scope.Database.ExecuteScalar( + Assert.AreEqual(4, ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", new { nodeId = content.Id, propTypeId = propertyTypeId })); scope.Complete(); @@ -696,7 +696,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services int propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; using (IScope scope = ScopeProvider.CreateScope()) { - Assert.AreEqual(4, scope.Database.ExecuteScalar( + Assert.AreEqual(4, ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", new { nodeId = content.Id, propTypeId = propertyTypeId })); @@ -727,7 +727,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services int propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; using (IScope scope = ScopeProvider.CreateScope()) { - Assert.AreEqual(5, scope.Database.ExecuteScalar( + Assert.AreEqual(5, ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", new { nodeId = content.Id, propTypeId = propertyTypeId })); @@ -758,7 +758,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services int propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; using (IScope scope = ScopeProvider.CreateScope()) { - Assert.AreEqual(2, scope.Database.ExecuteScalar( + Assert.AreEqual(2, ScopeAccessor.AmbientScope.Database.ExecuteScalar( "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", new { nodeId = content.Id, propTypeId = propertyTypeId })); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index 04617ae5f3..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) @@ -75,7 +62,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { Sql selectJson = SqlContext.Sql().Select().From().Where(x => x.NodeId == id && !x.Published); - ContentNuDto dto = scope.Database.Fetch(selectJson).FirstOrDefault(); + ContentNuDto dto = ScopeAccessor.AmbientScope.Database.Fetch(selectJson).FirstOrDefault(); Assert.IsNotNull(dto); string json = dto.Data; return json; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs index 08200a6f7e..97a5698ef0 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs @@ -77,9 +77,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { // SQL CE is fun! - var contentVersions = scope.Database.Single(@"select count(1) from umbracoContentVersion"); - var documentVersions = scope.Database.Single(@"select count(1) from umbracoDocumentVersion"); - var propertyData = scope.Database.Single(@"select count(1) from umbracoPropertyData"); + var contentVersions = ScopeAccessor.AmbientScope.Database.Single(@"select count(1) from umbracoContentVersion"); + var documentVersions = ScopeAccessor.AmbientScope.Database.Single(@"select count(1) from umbracoDocumentVersion"); + var propertyData = ScopeAccessor.AmbientScope.Database.Single(@"select count(1) from umbracoPropertyData"); return new Report { @@ -103,7 +103,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Updated = DateTime.Today }; - scope.Database.Insert(entity); + ScopeAccessor.AmbientScope.Database.Insert(entity); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs index b4f6c1ae5f..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")] @@ -36,16 +36,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services using (global::Umbraco.Cms.Core.Scoping.IScope scope = ScopeProvider.CreateScope()) { // insert duplicates manuall - scope.Database.Insert(new ExternalLoginDto + ScopeAccessor.AmbientScope.Database.Insert(new ExternalLoginDto { - UserId = user.Id, + UserOrMemberKey = user.Key, LoginProvider = "test1", ProviderKey = providerKey, CreateDate = latest }); - scope.Database.Insert(new ExternalLoginDto + 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.Infrastructure/Services/LocalizationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs index dee9ec819e..aca0ee4183 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs @@ -142,17 +142,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services currParentId = desc1.Key; } - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; IDictionaryItem[] items = LocalizationService.GetDictionaryItemDescendants(_parentItemGuidId).ToArray(); - Debug.WriteLine("SQL CALLS: " + scope.Database.AsUmbracoDatabase().SqlCount); + Debug.WriteLine("SQL CALLS: " + ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().SqlCount); Assert.AreEqual(51, items.Length); // There's a call or two to get languages, so apart from that there should only be one call per level. - Assert.Less(scope.Database.AsUmbracoDatabase().SqlCount, 30); + Assert.Less(ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().SqlCount, 30); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MacroServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MacroServiceTests.cs index 75dae7515b..1f847356a1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MacroServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MacroServiceTests.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs index 786a665509..16b9430ff7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs @@ -109,7 +109,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services 0, 11, out long total, - provider.SqlContext.Query() + provider.CreateQuery() .Where(x => new[] { mediaType1.Id, mediaType2.Id }.Contains(x.ContentTypeId)), Ordering.By("SortOrder", Direction.Ascending)); Assert.AreEqual(11, result.Count()); @@ -120,7 +120,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services 1, 11, out total, - provider.SqlContext.Query() + provider.CreateQuery() .Where(x => new[] { mediaType1.Id, mediaType2.Id }.Contains(x.ContentTypeId)), Ordering.By("SortOrder", Direction.Ascending)); Assert.AreEqual(9, result.Count()); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs index 96a91af10f..2fe543c1e2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs @@ -333,7 +333,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services int roleId; using (IScope scope = ScopeProvider.CreateScope()) { - roleId = scope.Database.ExecuteScalar("SELECT id from umbracoNode where [text] = 'MyTestRole1'"); + roleId = ScopeAccessor.AmbientScope.Database.ExecuteScalar("SELECT id from umbracoNode where [text] = 'MyTestRole1'"); scope.Complete(); } @@ -346,8 +346,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services using (IScope scope = ScopeProvider.CreateScope()) { - scope.Database.Insert(new Member2MemberGroupDto { MemberGroup = roleId, Member = member1.Id }); - scope.Database.Insert(new Member2MemberGroupDto { MemberGroup = roleId, Member = member2.Id }); + ScopeAccessor.AmbientScope.Database.Insert(new Member2MemberGroupDto { MemberGroup = roleId, Member = member1.Id }); + ScopeAccessor.AmbientScope.Database.Insert(new Member2MemberGroupDto { MemberGroup = roleId, Member = member2.Id }); scope.Complete(); } @@ -1281,7 +1281,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services List colResult; using (IScope scope = ScopeProvider.CreateScope()) { - colResult = scope.Database.Fetch(sql); + colResult = ScopeAccessor.AmbientScope.Database.Fetch(sql); scope.Complete(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs index 1c10f31e18..a0abfd70ee 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.Linq; @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs index 9eded84598..7611bea687 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs @@ -54,7 +54,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { using (IScope scope = ScopeProvider.CreateScope()) { - scope.Database.Execute("SET LOCK_TIMEOUT 60000"); + ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); service.Save(content); scope.Complete(); } @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { using (IScope scope = ScopeProvider.CreateScope()) { - scope.Database.Execute("SET LOCK_TIMEOUT 60000"); + ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); service.Save(media); scope.Complete(); } 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.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs index 4e4ce29e9a..ecbd8d52aa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -21,6 +22,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers [TestFixture] public class EntityControllerTests : UmbracoTestServerTestBase { + private IScopeProvider ScopeProvider => GetRequiredService(); + [Test] public async Task GetUrlsByIds_MediaWithIntegerIds_ReturnsValidMap() { 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/PublishedSnapshotServiceTestBase.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs index 4cf6ffdc9e..cfe3830970 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs @@ -230,7 +230,6 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers .Setup(x => x.CreateScope( It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), 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/Cache/DefaultCachePolicyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DefaultCachePolicyTests.cs index 2caa33aa3f..85e1c9a221 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DefaultCachePolicyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DefaultCachePolicyTests.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache { @@ -19,7 +20,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache get { var accessor = new Mock(); - var scope = new Mock(); + var scope = new Mock(); scope.Setup(x => x.RepositoryCacheMode).Returns(RepositoryCacheMode.Default); accessor.Setup(x => x.AmbientScope).Returns(scope.Object); return accessor.Object; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FullDataSetCachePolicyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FullDataSetCachePolicyTests.cs index d4b91a196f..578e133a16 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FullDataSetCachePolicyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FullDataSetCachePolicyTests.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache { @@ -22,7 +23,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache get { var accessor = new Mock(); - var scope = new Mock(); + var scope = new Mock(); scope.Setup(x => x.RepositoryCacheMode).Returns(RepositoryCacheMode.Default); accessor.Setup(x => x.AmbientScope).Returns(scope.Object); return accessor.Object; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/SingleItemsOnlyCachePolicyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/SingleItemsOnlyCachePolicyTests.cs index 4696dc708c..20c6d173c2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/SingleItemsOnlyCachePolicyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/SingleItemsOnlyCachePolicyTests.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache { @@ -19,7 +20,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache get { var accessor = new Mock(); - var scope = new Mock(); + var scope = new Mock(); scope.Setup(x => x.RepositoryCacheMode).Returns(RepositoryCacheMode.Default); accessor.Setup(x => x.AmbientScope).Returns(scope.Object); return accessor.Object; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index 2fb4ce2f21..152808d6f3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -44,7 +44,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Components { // FIXME: use IUmbracoDatabaseFactory vs UmbracoDatabaseFactory, clean it all up! var mock = new Mock(); - NullLoggerFactory loggerFactory = NullLoggerFactory.Instance; + ILoggerFactory loggerFactory = NullLoggerFactory.Instance; ILogger logger = loggerFactory.CreateLogger("GenericLogger"); var globalSettings = new GlobalSettings(); var connectionStrings = new ConnectionStrings(); @@ -69,7 +69,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Components Mock.Of(), Options.Create(new ContentSettings())); IEventAggregator eventAggregator = Mock.Of(); - var scopeProvider = new ScopeProvider(f, fs, new TestOptionsMonitor(coreDebug), mediaFileManager, loggerFactory.CreateLogger(), loggerFactory, NoAppCache.Instance, eventAggregator); + var scopeProvider = new ScopeProvider(f, fs, new TestOptionsMonitor(coreDebug), loggerFactory, NoAppCache.Instance, eventAggregator); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(logger); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(loggerFactory.CreateLogger); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/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/BlockEditorComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs index b76719888f..efd753296a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs @@ -21,7 +21,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { Formatting = Formatting.None, - NullValueHandling = NullValueHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore }; private const string ContentGuid1 = "036ce82586a64dfba2d523a99ed80f58"; 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/Scoping/ScopeEventDispatcherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopeEventDispatcherTests.cs deleted file mode 100644 index 5cb21cac3c..0000000000 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopeEventDispatcherTests.cs +++ /dev/null @@ -1,452 +0,0 @@ -using System; -using System.Linq; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Tests.Common; -using Umbraco.Cms.Tests.Common.Builders; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Scoping -{ - [TestFixture] - public class ScopeEventDispatcherTests - { - [SetUp] - public void Setup() - { - // remove all handlers first - DoThing1 = null; - DoThing2 = null; - DoThing3 = null; - } - - - - [TestCase(false, true, true)] - [TestCase(false, true, false)] - [TestCase(false, false, true)] - [TestCase(false, false, false)] - [TestCase(true, true, true)] - [TestCase(true, true, false)] - [TestCase(true, false, true)] - [TestCase(true, false, false)] - public void EventsHandling(bool passive, bool cancel, bool complete) - { - var counter1 = 0; - var counter2 = 0; - - DoThing1 += (sender, args) => { counter1++; if (cancel) args.Cancel = true; }; - DoThing2 += (sender, args) => { counter2++; }; - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); - using (var scope = scopeProvider.CreateScope(eventDispatcher: passive ? new PassiveEventDispatcher() : null)) - { - var cancelled = scope.Events.DispatchCancelable(DoThing1, this, new SaveEventArgs("test")); - if (cancelled == false) - scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); - if (complete) - scope.Complete(); - } - - var expected1 = passive ? 0 : 1; - Assert.AreEqual(expected1, counter1); - - int expected2; - if (passive) - expected2 = 0; - else - expected2 = cancel ? 0 : (complete ? 1 : 0); - - Assert.AreEqual(expected2, counter2); - } - - private ScopeProvider GetScopeProvider(NullLoggerFactory instance) - { - var fileSystems = new FileSystems( - instance, - Mock.Of(), - Options.Create(new GlobalSettings()), - Mock.Of()); - - var mediaFileManager = new MediaFileManager( - Mock.Of(), - Mock.Of(), - instance.CreateLogger(), - Mock.Of(), - Mock.Of(), - Options.Create(new ContentSettings())); - - return new ScopeProvider( - Mock.Of(), - fileSystems, - new TestOptionsMonitor(new CoreDebugSettings()), - mediaFileManager, - Mock.Of>(), - instance, - Mock.Of(), - Mock.Of() - ); - } - - [Test] - public void QueueEvents() - { - DoThing1 += OnDoThingFail; - DoThing2 += OnDoThingFail; - DoThing3 += OnDoThingFail; - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); - using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { - scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); - scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); - scope.Events.Dispatch(DoThing3, this, new SaveEventArgs(0)); - - // events have been queued - Assert.AreEqual(3, scope.Events.GetEvents(EventDefinitionFilter.All).Count()); - - var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); - - var knownNames = new[] { "DoThing1", "DoThing2", "DoThing3" }; - var knownArgTypes = new[] { typeof(SaveEventArgs), typeof(SaveEventArgs), typeof(SaveEventArgs) }; - - for (var i = 0; i < events.Length; i++) - { - Assert.AreEqual(knownNames[i], events[i].EventName); - Assert.AreEqual(knownArgTypes[i], events[i].Args.GetType()); - } - } - } - - [Test] - public void SupersededEvents() - { - DoSaveForContent += OnDoThingFail; - DoDeleteForContent += OnDoThingFail; - DoForTestArgs += OnDoThingFail; - DoForTestArgs2 += OnDoThingFail; - - var contentType = ContentTypeBuilder.CreateBasicContentType(); - - var content1 = ContentBuilder.CreateBasicContent(contentType); - content1.Id = 123; - - var content2 = ContentBuilder.CreateBasicContent(contentType); - content2.Id = 456; - - var content3 = ContentBuilder.CreateBasicContent(contentType); - content3.Id = 789; - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); - using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { - - //content1 will be filtered from the args - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(new[] { content1, content3 })); - scope.Events.Dispatch(DoDeleteForContent, this, new DeleteEventArgs(content1), "DoDeleteForContent"); - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); - //this entire event will be filtered - scope.Events.Dispatch(DoForTestArgs, this, new TestEventArgs(content1)); - scope.Events.Dispatch(DoForTestArgs2, this, new TestEventArgs2(content1)); - - // events have been queued - var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); - Assert.AreEqual(4, events.Length); - - Assert.AreEqual(typeof(SaveEventArgs), events[0].Args.GetType()); - Assert.AreEqual(1, ((SaveEventArgs)events[0].Args).SavedEntities.Count()); - Assert.AreEqual(content3.Id, ((SaveEventArgs)events[0].Args).SavedEntities.First().Id); - - Assert.AreEqual(typeof(DeleteEventArgs), events[1].Args.GetType()); - Assert.AreEqual(content1.Id, ((DeleteEventArgs)events[1].Args).DeletedEntities.First().Id); - - Assert.AreEqual(typeof(SaveEventArgs), events[2].Args.GetType()); - Assert.AreEqual(content2.Id, ((SaveEventArgs)events[2].Args).SavedEntities.First().Id); - - Assert.AreEqual(typeof(TestEventArgs2), events[3].Args.GetType()); - } - } - - [Test] - public void SupersededEvents2() - { - Test_Unpublished += OnDoThingFail; - Test_Deleted += OnDoThingFail; - - var contentService = Mock.Of(); - var content = Mock.Of(); - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); - using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { - scope.Events.Dispatch(Test_Unpublished, contentService, new PublishEventArgs(new[] { content }), "Unpublished"); - scope.Events.Dispatch(Test_Deleted, contentService, new DeleteEventArgs(new[] { content }), "Deleted"); - - // see U4-10764 - var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); - Assert.AreEqual(2, events.Length); - } - } - - /// - /// This will test that when we track events that before we Get the events we normalize all of the - /// event entities to be the latest one (most current) found amongst the event so that there is - /// no 'stale' entities in any of the args - /// - [Test] - public void LatestEntities() - { - DoSaveForContent += OnDoThingFail; - - var now = DateTime.Now; - var contentType = ContentTypeBuilder.CreateBasicContentType(); - var content1 = ContentBuilder.CreateBasicContent(contentType); - content1.Id = 123; - content1.UpdateDate = now.AddMinutes(1); - var content2 = ContentBuilder.CreateBasicContent(contentType); - content2.Id = 123; - content2.UpdateDate = now.AddMinutes(2); - var content3 = ContentBuilder.CreateBasicContent(contentType); - content3.Id = 123; - content3.UpdateDate = now.AddMinutes(3); - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); - using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); - - // events have been queued - var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); - Assert.AreEqual(3, events.Length); - - foreach (var t in events) - { - var args = (SaveEventArgs)t.Args; - foreach (var entity in args.SavedEntities) - { - Assert.AreEqual(content3, entity); - Assert.IsTrue(object.ReferenceEquals(content3, entity)); - } - } - } - } - - [Test] - public void FirstIn() - { - DoSaveForContent += OnDoThingFail; - - var now = DateTime.Now; - var contentType = ContentTypeBuilder.CreateBasicContentType(); - var content1 = ContentBuilder.CreateBasicContent(contentType); - content1.Id = 123; - content1.UpdateDate = now.AddMinutes(1); - var content2 = ContentBuilder.CreateBasicContent(contentType); - content2.Id = 123; - content1.UpdateDate = now.AddMinutes(2); - var content3 = ContentBuilder.CreateBasicContent(contentType); - content3.Id = 123; - content1.UpdateDate = now.AddMinutes(3); - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); - using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); - - // events have been queued - var events = scope.Events.GetEvents(EventDefinitionFilter.FirstIn).ToArray(); - Assert.AreEqual(1, events.Length); - Assert.AreEqual(content1, ((SaveEventArgs)events[0].Args).SavedEntities.First()); - Assert.IsTrue(object.ReferenceEquals(content1, ((SaveEventArgs)events[0].Args).SavedEntities.First())); - Assert.AreEqual(content1.UpdateDate, ((SaveEventArgs)events[0].Args).SavedEntities.First().UpdateDate); - } - } - - [Test] - public void LastIn() - { - DoSaveForContent += OnDoThingFail; - - var now = DateTime.Now; - var contentType = ContentTypeBuilder.CreateBasicContentType(); - var content1 = ContentBuilder.CreateBasicContent(contentType); - content1.Id = 123; - content1.UpdateDate = now.AddMinutes(1); - var content2 = ContentBuilder.CreateBasicContent(contentType); - content2.Id = 123; - content2.UpdateDate = now.AddMinutes(2); - var content3 = ContentBuilder.CreateBasicContent(contentType); - content3.Id = 123; - content3.UpdateDate = now.AddMinutes(3); - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); - using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); - - // events have been queued - var events = scope.Events.GetEvents(EventDefinitionFilter.LastIn).ToArray(); - Assert.AreEqual(1, events.Length); - Assert.AreEqual(content3, ((SaveEventArgs)events[0].Args).SavedEntities.First()); - Assert.IsTrue(object.ReferenceEquals(content3, ((SaveEventArgs)events[0].Args).SavedEntities.First())); - Assert.AreEqual(content3.UpdateDate, ((SaveEventArgs)events[0].Args).SavedEntities.First().UpdateDate); - } - } - - [TestCase(true)] - [TestCase(false)] - public void EventsDispatching_Passive(bool complete) - { - DoThing1 += OnDoThingFail; - DoThing2 += OnDoThingFail; - DoThing3 += OnDoThingFail; - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); - using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { - scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); - scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); - scope.Events.Dispatch(DoThing3, this, new SaveEventArgs(0)); - - // events have been queued - Assert.AreEqual(3, scope.Events.GetEvents(EventDefinitionFilter.All).Count()); - - if (complete) - scope.Complete(); - } - - // no event has been raised (else OnDoThingFail would have failed) - } - - [TestCase(true)] - [TestCase(false)] - public void EventsDispatching_Scope(bool complete) - { - var counter = 0; - IScope ambientScope = null; - IScopeContext ambientContext = null; - Guid value = Guid.Empty; - - var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance) as ScopeProvider; - - DoThing1 += (sender, args) => { counter++; }; - DoThing2 += (sender, args) => { counter++; }; - DoThing3 += (sender, args) => - { - ambientScope = scopeProvider.AmbientScope; - ambientContext = scopeProvider.AmbientContext; - value = scopeProvider.Context.Enlist("value", Guid.NewGuid, (c, o) => { }); - counter++; - }; - - Guid guid; - using (var scope = scopeProvider.CreateScope()) - { - Assert.IsNotNull(scopeProvider.AmbientContext); - guid = scopeProvider.Context.Enlist("value", Guid.NewGuid, (c, o) => { }); - - scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); - scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); - scope.Events.Dispatch(DoThing3, this, new SaveEventArgs(0)); - - // events have been queued - Assert.AreEqual(3, scope.Events.GetEvents(EventDefinitionFilter.All).Count()); - Assert.AreEqual(0, counter); - - if (complete) - scope.Complete(); - } - - if (complete) - { - // events have been raised - Assert.AreEqual(3, counter); - Assert.IsNull(ambientScope); // scope was gone - Assert.IsNotNull(ambientContext); // but not context - Assert.AreEqual(guid, value); // so we got the same value! - } - else - { - // else, no event has been raised - Assert.AreEqual(0, counter); - } - - // everything's gone - Assert.IsNull(scopeProvider.AmbientScope); - Assert.IsNull(scopeProvider.AmbientContext); - } - - private static void OnDoThingFail(object sender, EventArgs eventArgs) - { - Assert.Fail(); - } - - public static event EventHandler> DoSaveForContent; - public static event EventHandler> DoDeleteForContent; - public static event EventHandler DoForTestArgs; - public static event EventHandler DoForTestArgs2; - public static event EventHandler> DoThing1; - public static event EventHandler> DoThing2; - - public static event TypedEventHandler> DoThing3; - - public static event TypedEventHandler> Test_Unpublished; - public static event TypedEventHandler> Test_Deleted; - - public class TestEventArgs : CancellableObjectEventArgs - { - public TestEventArgs(object eventObject) : base(eventObject) - { - } - - public object MyEventObject - { - get { return EventObject; } - } - } - - [SupersedeEvent(typeof(TestEventArgs))] - public class TestEventArgs2 : CancellableObjectEventArgs - { - public TestEventArgs2(object eventObject) : base(eventObject) - { - } - - public object MyEventObject - { - get { return EventObject; } - } - } - - public class PassiveEventDispatcher : QueuingEventDispatcherBase - { - public PassiveEventDispatcher() - : base(false) - { } - - protected override void ScopeExitCompleted() - { - // do nothing - } - } - } -} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs index 5e6b3fe382..6d614b62e9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs @@ -94,8 +94,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Scoping Mock.Of(), fileSystems, new TestOptionsMonitor(new CoreDebugSettings()), - mediaFileManager, - loggerFactory.CreateLogger(), loggerFactory, Mock.Of(), eventAggregatorMock.Object diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs index 801c27110d..6eb9af166f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs @@ -8,7 +8,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Infrastructure.Services.Implement; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.UnitTests.AutoFixture; using ContentVersionCleanupPolicySettings = Umbraco.Cms.Core.Models.ContentVersionCleanupPolicySettings; 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/HostedServices/LogScrubberTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/LogScrubberTests.cs index 055224e32f..fc61f90150 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/LogScrubberTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/LogScrubberTests.cs @@ -77,7 +77,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices var mockScope = new Mock(); var mockScopeProvider = new Mock(); mockScopeProvider - .Setup(x => x.CreateScope(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.CreateScope(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(mockScope.Object); var mockLogger = new Mock>(); var mockProfilingLogger = new Mock(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs index 793acdfa3e..a281225990 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; using Umbraco.Cms.Tests.Common; @@ -52,7 +53,15 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.Serv VerifyServerTouched(); } - private TouchServerTask CreateTouchServerTask(RuntimeLevel runtimeLevel = RuntimeLevel.Run, string applicationUrl = ApplicationUrl) + [Test] + public async Task Does_Not_Execute_When_Role_Accessor_Is_Not_Elected() + { + TouchServerTask sut = CreateTouchServerTask(useElection: false); + await sut.PerformExecuteAsync(null); + VerifyServerNotTouched(); + } + + private TouchServerTask CreateTouchServerTask(RuntimeLevel runtimeLevel = RuntimeLevel.Run, string applicationUrl = ApplicationUrl, bool useElection = true) { var mockRequestAccessor = new Mock(); mockRequestAccessor.SetupGet(x => x.ApplicationMainUrl).Returns(!string.IsNullOrEmpty(applicationUrl) ? new Uri(ApplicationUrl) : null); @@ -72,12 +81,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.Serv } }; + IServerRoleAccessor roleAccessor = useElection + ? new ElectedServerRoleAccessor(_mockServerRegistrationService.Object) + : new SingleServerRoleAccessor(); + return new TouchServerTask( mockRunTimeState.Object, _mockServerRegistrationService.Object, mockRequestAccessor.Object, mockLogger.Object, - new TestOptionsMonitor(settings)); + new TestOptionsMonitor(settings), + roleAccessor); } private void VerifyServerNotTouched() => VerifyServerTouchedTimes(Times.Never()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Mapping/MappingTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Mapping/MappingTests.cs index 9c8f9da75d..f707464443 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Mapping/MappingTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Mapping/MappingTests.cs @@ -29,7 +29,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Mapping scopeMock.Setup(x => x.CreateScope( It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index 08bcee255e..2e1ac83cd8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.UnitTests.TestHelpers; using Umbraco.Extensions; @@ -34,7 +35,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations NullLoggerFactory loggerFactory = NullLoggerFactory.Instance; var database = new TestDatabase(); - IScope scope = Mock.Of(x => x.Notifications == Mock.Of()); + IDatabaseScope scope = Mock.Of(x => x.Notifications == Mock.Of()); Mock.Get(scope) .Setup(x => x.Database) .Returns(database); @@ -58,7 +59,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations } }); - var executor = new MigrationPlanExecutor(scopeProvider, loggerFactory, migrationBuilder); + var executor = new MigrationPlanExecutor(scopeProvider, scopeProvider, loggerFactory, migrationBuilder); MigrationPlan plan = new MigrationPlan("default") .From(string.Empty) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationTests.cs index 988262cfd5..2f2e36221c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationTests.cs @@ -10,25 +10,26 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations { [TestFixture] public class MigrationTests { - public class TestScopeProvider : IScopeProvider + public class TestScopeProvider : IScopeProvider, IScopeAccessor { - private readonly IScope _scope; + private readonly IDatabaseScope _scope; - public TestScopeProvider(IScope scope) => _scope = scope; + public TestScopeProvider(IDatabaseScope scope) => _scope = scope; public IScope CreateScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher notificationPublisher = null, bool? scopeFileSystems = null, bool callContext = false, @@ -37,7 +38,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations public IScope CreateDetachedScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher eventDispatcher = null, IScopedNotificationPublisher notificationPublisher = null, bool? scopeFileSystems = null) => throw new NotImplementedException(); @@ -46,6 +46,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations public IScope DetachScope() => throw new NotImplementedException(); public IScopeContext Context { get; set; } + public IQuery CreateQuery() => SqlContext.Query(); public ISqlContext SqlContext { get; set; } @@ -56,6 +57,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations } public IEnumerable ScopeInfos => throw new NotImplementedException(); #endif + public IDatabaseScope AmbientScope => _scope; } private class TestPlan : MigrationPlan diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs index a61de49ee5..09d0cb99d1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs @@ -18,6 +18,7 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.TestHelpers; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations @@ -26,8 +27,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations public class PostMigrationTests { private static readonly ILoggerFactory s_loggerFactory = NullLoggerFactory.Instance; - private IMigrationPlanExecutor GetMigrationPlanExecutor(IScopeProvider scopeProvider, IMigrationBuilder builder) - => new MigrationPlanExecutor(scopeProvider, s_loggerFactory, builder); + private IMigrationPlanExecutor GetMigrationPlanExecutor(IScopeProvider scopeProvider, IScopeAccessor scopeAccessor,IMigrationBuilder builder) + => new MigrationPlanExecutor(scopeProvider, scopeAccessor, s_loggerFactory, builder); [Test] public void ExecutesPlanPostMigration() @@ -49,7 +50,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations }); var database = new TestDatabase(); - IScope scope = Mock.Of(x => x.Notifications == Mock.Of()); + IDatabaseScope scope = Mock.Of(x => x.Notifications == Mock.Of()); Mock.Get(scope) .Setup(x => x.Database) .Returns(database); @@ -67,7 +68,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations TestPostMigration.MigrateCount = 0; var upgrader = new Upgrader(plan); - IMigrationPlanExecutor executor = GetMigrationPlanExecutor(scopeProvider, builder); + IMigrationPlanExecutor executor = GetMigrationPlanExecutor(scopeProvider, scopeProvider, builder); upgrader.Execute( executor, scopeProvider, @@ -98,7 +99,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations }); var database = new TestDatabase(); - IScope scope = Mock.Of(x => x.Notifications == Mock.Of()); + IDatabaseScope scope = Mock.Of(x => x.Notifications == Mock.Of()); Mock.Get(scope) .Setup(x => x.Database) .Returns(database); @@ -118,7 +119,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations new MigrationContext(plan, database, s_loggerFactory.CreateLogger()); var upgrader = new Upgrader(plan); - IMigrationPlanExecutor executor = GetMigrationPlanExecutor(scopeProvider, builder); + IMigrationPlanExecutor executor = GetMigrationPlanExecutor(scopeProvider, scopeProvider, builder); upgrader.Execute( executor, scopeProvider, 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/NPocoSqlExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs index 92258c46e7..222b2c2b36 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using NPoco; using NUnit.Framework; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; 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/Persistence/NPocoTests/NPocoSqlTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTests.cs index 103f9207d0..e826af74fd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTests.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using NPoco; using NUnit.Framework; +using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs index cc4532580a..6ef176ceef 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs @@ -61,8 +61,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping databaseFactory.Object, fileSystems, new TestOptionsMonitor(new CoreDebugSettings()), - mediaFileManager, - loggerFactory.CreateLogger(), loggerFactory, Mock.Of(), Mock.Of()); 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 db58239e5f..a7ad27f1a6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs @@ -29,7 +29,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security var mockScope = new Mock(); var mockScopeProvider = new Mock(); mockScopeProvider - .Setup(x => x.CreateScope(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.CreateScope(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(mockScope.Object); return new MemberUserStore( @@ -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.Infrastructure/Services/AmbiguousEventTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/AmbiguousEventTests.cs index fa62ad8fc3..ad4cb80113 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/AmbiguousEventTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/AmbiguousEventTests.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Text; using NUnit.Framework; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Services diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/LocalizedTextServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/LocalizedTextServiceTests.cs index 497af66d63..848d634409 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/LocalizedTextServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/LocalizedTextServiceTests.cs @@ -9,6 +9,7 @@ using System.Xml.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Services 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 da5175f272..aaf3445908 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -523,7 +523,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var scopeProvider = Mock.Of(x => x.CreateScope( It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), @@ -618,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( diff --git a/umbraco.sln b/umbraco.sln index 0018c91041..497258c699 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29209.152 @@ -38,7 +37,7 @@ EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}" ProjectSection(WebsiteProperties) = preProject UseIISExpress = "true" - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5.2" Debug.AspNetCompiler.VirtualPath = "/localhost_3961" Debug.AspNetCompiler.PhysicalPath = "src\Umbraco.Web.UI.Client\" Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_3961\" @@ -61,7 +60,7 @@ EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "http://localhost:58896", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" ProjectSection(WebsiteProperties) = preProject UseIISExpress = "true" - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5.2" Debug.AspNetCompiler.VirtualPath = "/localhost_62926" Debug.AspNetCompiler.PhysicalPath = "tests\Umbraco.Tests.AcceptanceTest\" Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\"