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