diff --git a/.github/config/codeql-config.yml b/.github/config/codeql-config.yml
index 432be995a5..77b390d392 100644
--- a/.github/config/codeql-config.yml
+++ b/.github/config/codeql-config.yml
@@ -5,3 +5,4 @@ paths:
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 9ec8c3b4e3..33d3e851c7 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -27,7 +27,7 @@ jobs:
with:
dotnet-version: '6.0.x'
- - name: Build
+ - name: dotnet build
run: dotnet build umbraco-netcore-only.sln # also runs npm build
- name: Perform CodeQL Analysis
diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs
index a9b70fc4a1..048513a5da 100644
--- a/src/JsonSchema/AppSettings.cs
+++ b/src/JsonSchema/AppSettings.cs
@@ -86,6 +86,8 @@ namespace JsonSchema
public PackageMigrationSettings PackageMigration { get; set; }
public LegacyPasswordMigrationSettings LegacyPasswordMigration { get; set; }
+
+ public ContentDashboardSettings ContentDashboard { get; set; }
}
///
diff --git a/src/Umbraco.Core/Configuration/Models/CharItem.cs b/src/Umbraco.Core/Configuration/Models/CharItem.cs
new file mode 100644
index 0000000000..e269e0a83e
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/Models/CharItem.cs
@@ -0,0 +1,17 @@
+using Umbraco.Cms.Core.Configuration.UmbracoSettings;
+
+namespace Umbraco.Cms.Core.Configuration.Models
+{
+ public class CharItem : IChar
+ {
+ ///
+ /// The character to replace
+ ///
+ public string Char { get; set; }
+
+ ///
+ /// The replacement character
+ ///
+ public string Replacement { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs
similarity index 55%
rename from src/Umbraco.Core/Configuration/ContentDashboardSettings.cs
rename to src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs
index 7bef36dba4..3f8546a1ad 100644
--- a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs
@@ -1,6 +1,11 @@
-
-namespace Umbraco.Cms.Core.Configuration
+using System.ComponentModel;
+
+namespace Umbraco.Cms.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for content dashboard settings.
+ ///
+ [UmbracoOptions(Constants.Configuration.ConfigContentDashboard)]
public class ContentDashboardSettings
{
private const string DefaultContentDashboardPath = "cms";
@@ -18,6 +23,13 @@ namespace Umbraco.Cms.Core.Configuration
/// Gets the path to use when constructing the URL for retrieving data for the content dashboard.
///
/// The URL path.
- public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath;
+ [DefaultValue(DefaultContentDashboardPath)]
+ public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath;
+
+ ///
+ /// Gets the allowed addresses to retrieve data for the content dashboard.
+ ///
+ /// The URLs.
+ public string[] ContentDashboardUrlAllowlist { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
index ee223b36c6..051c31dc26 100644
--- a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using Umbraco.Cms.Core.Configuration.UmbracoSettings;
@@ -16,33 +17,34 @@ namespace Umbraco.Cms.Core.Configuration.Models
{
internal const bool StaticAddTrailingSlash = true;
internal const string StaticConvertUrlsToAscii = "try";
+ internal const bool StaticEnableDefaultCharReplacements = true;
internal static readonly CharItem[] DefaultCharCollection =
{
- new CharItem { Char = " ", Replacement = "-" },
- new CharItem { Char = "\"", Replacement = string.Empty },
- new CharItem { Char = "'", Replacement = string.Empty },
- new CharItem { Char = "%", Replacement = string.Empty },
- new CharItem { Char = ".", Replacement = string.Empty },
- new CharItem { Char = ";", Replacement = string.Empty },
- new CharItem { Char = "/", Replacement = string.Empty },
- new CharItem { Char = "\\", Replacement = string.Empty },
- new CharItem { Char = ":", Replacement = string.Empty },
- new CharItem { Char = "#", Replacement = string.Empty },
- new CharItem { Char = "+", Replacement = "plus" },
- new CharItem { Char = "*", Replacement = "star" },
- new CharItem { Char = "&", Replacement = string.Empty },
- new CharItem { Char = "?", Replacement = string.Empty },
- new CharItem { Char = "æ", Replacement = "ae" },
- new CharItem { Char = "ä", Replacement = "ae" },
- new CharItem { Char = "ø", Replacement = "oe" },
- new CharItem { Char = "ö", Replacement = "oe" },
- new CharItem { Char = "å", Replacement = "aa" },
- new CharItem { Char = "ü", Replacement = "ue" },
- new CharItem { Char = "ß", Replacement = "ss" },
- new CharItem { Char = "|", Replacement = "-" },
- new CharItem { Char = "<", Replacement = string.Empty },
- new CharItem { Char = ">", Replacement = string.Empty }
+ new () { Char = " ", Replacement = "-" },
+ new () { Char = "\"", Replacement = string.Empty },
+ new () { Char = "'", Replacement = string.Empty },
+ new () { Char = "%", Replacement = string.Empty },
+ new () { Char = ".", Replacement = string.Empty },
+ new () { Char = ";", Replacement = string.Empty },
+ new () { Char = "/", Replacement = string.Empty },
+ new () { Char = "\\", Replacement = string.Empty },
+ new () { Char = ":", Replacement = string.Empty },
+ new () { Char = "#", Replacement = string.Empty },
+ new () { Char = "+", Replacement = "plus" },
+ new () { Char = "*", Replacement = "star" },
+ new () { Char = "&", Replacement = string.Empty },
+ new () { Char = "?", Replacement = string.Empty },
+ new () { Char = "æ", Replacement = "ae" },
+ new () { Char = "ä", Replacement = "ae" },
+ new () { Char = "ø", Replacement = "oe" },
+ new () { Char = "ö", Replacement = "oe" },
+ new () { Char = "å", Replacement = "aa" },
+ new () { Char = "ü", Replacement = "ue" },
+ new () { Char = "ß", Replacement = "ss" },
+ new () { Char = "|", Replacement = "-" },
+ new () { Char = "<", Replacement = string.Empty },
+ new () { Char = ">", Replacement = string.Empty }
};
///
@@ -67,41 +69,21 @@ namespace Umbraco.Cms.Core.Configuration.Models
///
public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try");
- // We need to special handle ":", as this character is special in keys
-
- // TODO: implement from configuration
-
- //// var collection = _configuration.GetSection(Prefix + "CharCollection").GetChildren()
- //// .Select(x => new CharItem()
- //// {
- //// Char = x.GetValue("Char"),
- //// Replacement = x.GetValue("Replacement"),
- //// }).ToArray();
-
- //// if (collection.Any() || _configuration.GetSection("Prefix").GetChildren().Any(x =>
- //// x.Key.Equals("CharCollection", StringComparison.OrdinalIgnoreCase)))
- //// {
- //// return collection;
- //// }
-
- //// return DefaultCharCollection;
+ ///
+ /// Disable all default character replacements
+ ///
+ [DefaultValue(StaticEnableDefaultCharReplacements)]
+ public bool EnableDefaultCharReplacements { get; set; } = StaticEnableDefaultCharReplacements;
///
- /// Gets or sets a value for the default character collection for replacements.
+ /// Add additional character replacements, or override defaults
///
- /// WB-TODO
+ [Obsolete("Use the GetCharReplacements extension method in the Umbraco.Extensions namespace instead. Scheduled for removal in V11")]
public IEnumerable CharCollection { get; set; } = DefaultCharCollection;
///
- /// Defines a character replacement.
+ /// Add additional character replacements, or override defaults
///
- public class CharItem : IChar
- {
- ///
- public string Char { get; set; }
-
- ///
- public string Replacement { get; set; }
- }
+ public IEnumerable UserDefinedCharCollection { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs
new file mode 100644
index 0000000000..a916febb93
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Configuration.Models;
+
+namespace Umbraco.Cms.Core.Configuration.UmbracoSettings
+{
+ public class CharacterReplacementEqualityComparer : IEqualityComparer
+ {
+ public bool Equals(IChar x, IChar y)
+ {
+ if (ReferenceEquals(x, y))
+ {
+ return true;
+ }
+
+ if (x is null)
+ {
+ return false;
+ }
+
+ if (y is null)
+ {
+ return false;
+ }
+
+ if (x.GetType() != y.GetType())
+ {
+ return false;
+ }
+
+ return x.Char == y.Char && x.Replacement == y.Replacement;
+ }
+
+ public int GetHashCode(IChar obj)
+ {
+ unchecked
+ {
+ return ((obj.Char != null ? obj.Char.GetHashCode() : 0) * 397) ^ (obj.Replacement != null ? obj.Replacement.GetHashCode() : 0);
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs
index 4073a12149..61e840245c 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs
@@ -1,8 +1,9 @@
-namespace Umbraco.Cms.Core.Configuration.UmbracoSettings
+namespace Umbraco.Cms.Core.Configuration.UmbracoSettings
{
public interface IChar
{
string Char { get; }
+
string Replacement { get; }
}
}
diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs
index 063d733821..ab951618e3 100644
--- a/src/Umbraco.Core/Constants-Configuration.cs
+++ b/src/Umbraco.Core/Constants-Configuration.cs
@@ -1,4 +1,4 @@
-namespace Umbraco.Cms.Core
+namespace Umbraco.Cms.Core
{
public static partial class Constants
{
@@ -54,6 +54,7 @@
public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword";
public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor";
public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration";
+ public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard";
}
}
}
diff --git a/src/Umbraco.Core/Constants-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/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
index ce2e4f2304..f0cbf7f95d 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
@@ -1,9 +1,12 @@
using System;
+using System.Collections.Generic;
using System.Reflection;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Configuration.Models.Validation;
+using Umbraco.Extensions;
namespace Umbraco.Cms.Core.DependencyInjection
{
@@ -82,7 +85,10 @@ namespace Umbraco.Cms.Core.DependencyInjection
.AddUmbracoOptions()
.AddUmbracoOptions()
.AddUmbracoOptions()
- .AddUmbracoOptions();
+ .AddUmbracoOptions()
+ .AddUmbracoOptions();
+
+ builder.Services.Configure(options => options.MergeReplacements(builder.Config));
return builder;
}
diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
index 9b3674b07b..bceddf1fd6 100644
--- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
+++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
@@ -62,6 +62,19 @@ namespace Umbraco.Extensions
return username;
}
+ public static string GetEmail(this IIdentity identity)
+ {
+ if (identity == null) throw new ArgumentNullException(nameof(identity));
+
+ string email = null;
+ if (identity is ClaimsIdentity claimsIdentity)
+ {
+ email = claimsIdentity.FindFirstValue(ClaimTypes.Email);
+ }
+
+ return email;
+ }
+
///
/// Returns the first claim value found in the for the given claimType
///
diff --git a/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs
new file mode 100644
index 0000000000..e9e6618f8c
--- /dev/null
+++ b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Configuration;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Configuration.UmbracoSettings;
+
+namespace Umbraco.Extensions
+{
+ ///
+ /// Get concatenated user and default character replacements
+ /// taking into account
+ ///
+ public static class RequestHandlerSettingsExtension
+ {
+ ///
+ /// Get concatenated user and default character replacements
+ /// taking into account
+ ///
+ public static IEnumerable GetCharReplacements(this RequestHandlerSettings requestHandlerSettings)
+ {
+ if (requestHandlerSettings.EnableDefaultCharReplacements is false)
+ {
+ return requestHandlerSettings.UserDefinedCharCollection ?? Enumerable.Empty();
+ }
+
+ if (requestHandlerSettings.UserDefinedCharCollection == null || requestHandlerSettings.UserDefinedCharCollection.Any() is false)
+ {
+ return RequestHandlerSettings.DefaultCharCollection;
+ }
+
+ return MergeUnique(requestHandlerSettings.UserDefinedCharCollection, RequestHandlerSettings.DefaultCharCollection);
+ }
+
+ ///
+ /// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection
+ ///
+ internal static void MergeReplacements(this RequestHandlerSettings requestHandlerSettings, IConfiguration configuration)
+ {
+ string sectionKey = $"{Constants.Configuration.ConfigRequestHandler}:";
+
+ IEnumerable charCollection = GetReplacements(
+ configuration,
+ $"{sectionKey}{nameof(RequestHandlerSettings.CharCollection)}");
+
+ IEnumerable userDefinedCharCollection = GetReplacements(
+ configuration,
+ $"{sectionKey}{nameof(requestHandlerSettings.UserDefinedCharCollection)}");
+
+ IEnumerable mergedCollection = MergeUnique(userDefinedCharCollection, charCollection);
+
+ requestHandlerSettings.UserDefinedCharCollection = mergedCollection;
+ }
+
+ private static IEnumerable GetReplacements(IConfiguration configuration, string key)
+ {
+ var replacements = new List();
+ IEnumerable config = configuration.GetSection(key).GetChildren();
+
+ foreach (IConfigurationSection section in config)
+ {
+ var @char = section.GetValue(nameof(CharItem.Char));
+ var replacement = section.GetValue(nameof(CharItem.Replacement));
+ replacements.Add(new CharItem { Char = @char, Replacement = replacement });
+ }
+
+ return replacements;
+ }
+
+ ///
+ /// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in alternativeReplacements
+ ///
+ private static IEnumerable MergeUnique(
+ IEnumerable priorityReplacements,
+ IEnumerable alternativeReplacements)
+ {
+ var priorityReplacementsList = priorityReplacements.ToList();
+ var alternativeReplacementsList = alternativeReplacements.ToList();
+
+ foreach (CharItem alternativeReplacement in alternativeReplacementsList)
+ {
+ foreach (CharItem priorityReplacement in priorityReplacementsList)
+ {
+ if (priorityReplacement.Char == alternativeReplacement.Char)
+ {
+ alternativeReplacement.Replacement = priorityReplacement.Replacement;
+ }
+ }
+ }
+
+ return priorityReplacementsList.Union(
+ alternativeReplacementsList,
+ new CharacterReplacementEqualityComparer());
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Extensions/TypeExtensions.cs b/src/Umbraco.Core/Extensions/TypeExtensions.cs
index 67a6dd1dce..c5bc99cae8 100644
--- a/src/Umbraco.Core/Extensions/TypeExtensions.cs
+++ b/src/Umbraco.Core/Extensions/TypeExtensions.cs
@@ -430,10 +430,16 @@ namespace Umbraco.Extensions
where T : Attribute
{
if (type == null) return Enumerable.Empty();
- return type.GetCustomAttributes(typeof (T), inherited).OfType();
+ return type.GetCustomAttributes(typeof(T), inherited).OfType();
}
- ///
+ public static bool HasCustomAttribute(this Type type, bool inherit)
+ where T : Attribute
+ {
+ return type.GetCustomAttribute(inherit) != null;
+ }
+
+ ///
/// Tries to return a value based on a property name for an object but ignores case sensitivity
///
///
diff --git a/src/Umbraco.Core/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/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs
index 390f644831..7168f99078 100644
--- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs
+++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs
@@ -68,12 +68,15 @@ namespace Umbraco.Extensions
switch (storageType)
{
case TagsStorageType.Csv:
- property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)), culture); // csv string
+ property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string
break;
case TagsStorageType.Json:
+ var updatedTags = currentTags.Union(trimmedTags).ToArray();
+ var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags);
+ property.SetValue(updatedValue, culture); // json array
+ break;
property.SetValue(serializer.Serialize(currentTags.Union(trimmedTags).ToArray()), culture); // json array
- break;
}
}
else
@@ -81,7 +84,7 @@ namespace Umbraco.Extensions
switch (storageType)
{
case TagsStorageType.Csv:
- property.SetValue(string.Join(delimiter.ToString(), trimmedTags), culture); // csv string
+ property.SetValue(string.Join(delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(), culture); // csv string
break;
case TagsStorageType.Json:
@@ -124,11 +127,13 @@ namespace Umbraco.Extensions
switch (storageType)
{
case TagsStorageType.Csv:
- property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)), culture); // csv string
+ property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string
break;
case TagsStorageType.Json:
- property.SetValue(serializer.Serialize(currentTags.Except(trimmedTags).ToArray()), culture); // json array
+ var updatedTags = currentTags.Except(trimmedTags).ToArray();
+ var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags);
+ property.SetValue(updatedValue, culture); // json array
break;
}
}
@@ -160,7 +165,7 @@ namespace Umbraco.Extensions
case TagsStorageType.Json:
try
{
- return serializer.Deserialize(value).Select(x => x.ToString().Trim());
+ return serializer.Deserialize(value).Select(x => x.Trim());
}
catch (Exception)
{
diff --git a/src/Umbraco.Core/Models/TwoFactorLogin.cs b/src/Umbraco.Core/Models/TwoFactorLogin.cs
new file mode 100644
index 0000000000..6ede9606e8
--- /dev/null
+++ b/src/Umbraco.Core/Models/TwoFactorLogin.cs
@@ -0,0 +1,13 @@
+using System;
+using Umbraco.Cms.Core.Models.Entities;
+
+namespace Umbraco.Cms.Core.Models
+{
+ public class TwoFactorLogin : EntityBase, ITwoFactorLogin
+ {
+ public string ProviderName { get; set; }
+ public string Secret { get; set; }
+ public Guid UserOrMemberKey { get; set; }
+ public bool Confirmed { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs b/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs
new file mode 100644
index 0000000000..980a531ffd
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Umbraco.Cms.Core.Notifications
+{
+ public class MemberTwoFactorRequestedNotification : INotification
+ {
+ public MemberTwoFactorRequestedNotification(Guid memberKey)
+ {
+ MemberKey = memberKey;
+ }
+
+ public Guid MemberKey { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs
new file mode 100644
index 0000000000..a3d38720d7
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs
@@ -0,0 +1,9 @@
+namespace Umbraco.Cms.Core.Notifications
+{
+ ///
+ /// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured.
+ ///
+ ///
+ public class UmbracoApplicationStartedNotification : INotification
+ { }
+}
diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs
index 4cbf0a55c6..dd60f9431c 100644
--- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs
+++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs
@@ -1,23 +1,23 @@
-// Copyright (c) Umbraco.
-// See LICENSE for more details.
-
namespace Umbraco.Cms.Core.Notifications
{
///
- /// Notification that occurs at the very end of the Umbraco boot
- /// process and after all initialize.
+ /// Notification that occurs at the very end of the Umbraco boot process (after all s are initialized).
///
+ ///
public class UmbracoApplicationStartingNotification : INotification
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The runtime level
public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel;
///
- /// Gets the runtime level of execution.
+ /// Gets the runtime level.
///
+ ///
+ /// The runtime level.
+ ///
public RuntimeLevel RuntimeLevel { get; }
}
}
diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs
new file mode 100644
index 0000000000..be4c6ccfd4
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs
@@ -0,0 +1,9 @@
+namespace Umbraco.Cms.Core.Notifications
+{
+ ///
+ /// Notification that occurs when Umbraco has completely shutdown.
+ ///
+ ///
+ public class UmbracoApplicationStoppedNotification : INotification
+ { }
+}
diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs
index db86a1e614..6d5234bbcc 100644
--- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs
+++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs
@@ -1,4 +1,9 @@
namespace Umbraco.Cms.Core.Notifications
{
- public class UmbracoApplicationStoppingNotification : INotification { }
+ ///
+ /// Notification that occurs when Umbraco is shutting down (after all s are terminated).
+ ///
+ ///
+ public class UmbracoApplicationStoppingNotification : INotification
+ { }
}
diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
index 37560b4c0a..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";
diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs
index a685ab67f1..7d9594a3c6 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs
@@ -1,15 +1,19 @@
+using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Core.Persistence.Repositories
{
+
public interface IExternalLoginRepository : IReadWriteQueryRepository, IQueryRepository
{
+
///
/// Replaces all external login providers for the user
///
///
///
+ [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")]
void Save(int userId, IEnumerable logins);
///
@@ -17,8 +21,9 @@ namespace Umbraco.Cms.Core.Persistence.Repositories
///
///
///
+ [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")]
void Save(int userId, IEnumerable tokens);
-
+ [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")]
void DeleteUserLogins(int memberId);
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs
new file mode 100644
index 0000000000..0a4b9e76cf
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Security;
+
+namespace Umbraco.Cms.Core.Persistence.Repositories
+{
+
+ ///
+ /// Repository for external logins with Guid as key, so it can be shared for members and users
+ ///
+ public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository, IQueryRepository
+ {
+ ///
+ /// Replaces all external login providers for the user/member key
+ ///
+ void Save(Guid userOrMemberKey, IEnumerable logins);
+
+ ///
+ /// Replaces all external login provider tokens for the providers specified for the user/member key
+ ///
+ void Save(Guid userOrMemberKey, IEnumerable tokens);
+
+ ///
+ /// Deletes all external logins for the specified the user/member key
+ ///
+ void DeleteUserLogins(Guid userOrMemberKey);
+ }
+}
diff --git a/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs
new file mode 100644
index 0000000000..63622f8e82
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Persistence.Repositories
+{
+ public interface ITwoFactorLoginRepository: IReadRepository, IWriteRepository
+ {
+ Task DeleteUserLoginsAsync(Guid userOrMemberKey);
+ Task DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName);
+
+ Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey);
+ }
+
+}
diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
index 07607be2b0..6d3e40067e 100644
--- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
@@ -197,6 +197,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
default:
throw new ArgumentOutOfRangeException();
}
+
return value.TryConvertTo(valueType);
}
@@ -232,6 +233,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
StaticApplicationLogging.Logger.LogWarning("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType));
return null;
}
+
return result.Result;
}
diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
index 2a7bc97650..cc86fff004 100644
--- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
+++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
@@ -156,7 +156,7 @@ namespace Umbraco.Cms.Core.Routing
: DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture);
var defaultCulture = _localizationService.GetDefaultLanguageIsoCode();
- if (domainUri is not null || culture == defaultCulture || culture is null)
+ if (domainUri is not null || culture == defaultCulture || string.IsNullOrEmpty(culture))
{
var url = AssembleUrl(domainUri, path, current, mode).ToString();
return UrlInfo.Url(url, culture);
diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs
index ec4e56df1b..08d11db5cd 100644
--- a/src/Umbraco.Core/Runtime/MainDom.cs
+++ b/src/Umbraco.Core/Runtime/MainDom.cs
@@ -154,11 +154,11 @@ namespace Umbraco.Cms.Core.Runtime
// the handler is not installed so that would be the hosting environment
if (_signaled)
{
- _logger.LogInformation("Cannot acquire (signaled).");
+ _logger.LogInformation("Cannot acquire MainDom (signaled).");
return false;
}
- _logger.LogInformation("Acquiring.");
+ _logger.LogInformation("Acquiring MainDom.");
// Get the lock
var acquired = false;
@@ -168,12 +168,12 @@ namespace Umbraco.Cms.Core.Runtime
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error while acquiring");
+ _logger.LogError(ex, "Error while acquiring MainDom");
}
if (!acquired)
{
- _logger.LogInformation("Cannot acquire (timeout).");
+ _logger.LogInformation("Cannot acquire MainDom (timeout).");
// In previous versions we'd let a TimeoutException be thrown
// and the appdomain would not start. We have the opportunity to allow it to
@@ -209,7 +209,7 @@ namespace Umbraco.Cms.Core.Runtime
_logger.LogWarning(ex, ex.Message);
}
- _logger.LogInformation("Acquired.");
+ _logger.LogInformation("Acquired MainDom.");
return true;
}
diff --git a/src/Umbraco.Core/Security/IIdentityUserLogin.cs b/src/Umbraco.Core/Security/IIdentityUserLogin.cs
index 67ca739509..4e18771a17 100644
--- a/src/Umbraco.Core/Security/IIdentityUserLogin.cs
+++ b/src/Umbraco.Core/Security/IIdentityUserLogin.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Security
string ProviderKey { get; set; }
///
- /// Gets or sets user Id for the user who owns this login
+ /// Gets or sets user or member key (Guid) for the user/member who owns this login
///
string UserId { get; set; } // TODO: This should be able to be used by both users and members
diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs
index 787631d500..75f8069f0c 100644
--- a/src/Umbraco.Core/Services/IExternalLoginService.cs
+++ b/src/Umbraco.Core/Services/IExternalLoginService.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Security;
@@ -6,6 +7,7 @@ namespace Umbraco.Cms.Core.Services
///
/// Used to store the external login info
///
+ [Obsolete("Use IExternalLoginServiceWithKey. This will be removed in Umbraco 10")]
public interface IExternalLoginService : IService
{
///
diff --git a/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
new file mode 100644
index 0000000000..bc31f54f8b
--- /dev/null
+++ b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Security;
+
+namespace Umbraco.Cms.Core.Services
+{
+ public interface IExternalLoginWithKeyService : IService
+ {
+ ///
+ /// Returns all user logins assigned
+ ///
+ IEnumerable GetExternalLogins(Guid userOrMemberKey);
+
+ ///
+ /// Returns all user login tokens assigned
+ ///
+ IEnumerable GetExternalLoginTokens(Guid userOrMemberKey);
+
+ ///
+ /// Returns all logins matching the login info - generally there should only be one but in some cases
+ /// there might be more than one depending on if an administrator has been editing/removing members
+ ///
+ IEnumerable Find(string loginProvider, string providerKey);
+
+ ///
+ /// Saves the external logins associated with the user
+ ///
+ ///
+ /// The user or member key associated with the logins
+ ///
+ ///
+ ///
+ /// This will replace all external login provider information for the user
+ ///
+ void Save(Guid userOrMemberKey, IEnumerable logins);
+
+ ///
+ /// Saves the external login tokens associated with the user
+ ///
+ ///
+ /// The user or member key associated with the logins
+ ///
+ ///
+ ///
+ /// This will replace all external login tokens for the user
+ ///
+ void Save(Guid userOrMemberKey,IEnumerable tokens);
+
+ ///
+ /// Deletes all user logins - normally used when a member is deleted
+ ///
+ void DeleteUserLogins(Guid userOrMemberKey);
+ }
+}
diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs
new file mode 100644
index 0000000000..dd11f864fb
--- /dev/null
+++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services
+{
+ public interface ITwoFactorLoginService : IService
+ {
+ ///
+ /// Deletes all user logins - normally used when a member is deleted
+ ///
+ Task DeleteUserLoginsAsync(Guid userOrMemberKey);
+
+ Task IsTwoFactorEnabledAsync(Guid userKey);
+ Task GetSecretForUserAndProviderAsync(Guid userKey, string providerName);
+
+ Task