diff --git a/.gitignore b/.gitignore
index dc6f431550..b87d1df346 100644
--- a/.gitignore
+++ b/.gitignore
@@ -169,6 +169,7 @@ cypress.env.json
/src/Umbraco.Tests.AcceptanceTest/package-lock.json
/src/Umbraco.Tests.AcceptanceTest/cypress/videos/
/src/Umbraco.Tests.AcceptanceTest/cypress/screenshots/
+src/Umbraco.Web.UI/Umbraco/telemetrics-id.umb
# eof
diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec
index 2fea306ce0..fe5ef6344c 100644
--- a/build/NuSpecs/UmbracoCms.Web.nuspec
+++ b/build/NuSpecs/UmbracoCms.Web.nuspec
@@ -30,8 +30,8 @@
-
-
+
+
diff --git a/linting/codeanalysis.ruleset b/linting/codeanalysis.ruleset
index 4fde2bef8d..57c9fb7d60 100644
--- a/linting/codeanalysis.ruleset
+++ b/linting/codeanalysis.ruleset
@@ -11,6 +11,8 @@
+
+
\ No newline at end of file
diff --git a/src/Umbraco.Core/BackOffice/BackOfficeIdentityUser.cs b/src/Umbraco.Core/BackOffice/BackOfficeIdentityUser.cs
deleted file mode 100644
index 027e7c0904..0000000000
--- a/src/Umbraco.Core/BackOffice/BackOfficeIdentityUser.cs
+++ /dev/null
@@ -1,449 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-using System.ComponentModel;
-using System.Linq;
-using Umbraco.Core.Configuration;
-using Umbraco.Core.Configuration.Models;
-using Umbraco.Core.Models.Entities;
-using Umbraco.Core.Models.Identity;
-using Umbraco.Core.Models.Membership;
-
-namespace Umbraco.Core.BackOffice
-{
- public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty
- {
- private string _email;
- private string _userName;
- private int _id;
- private bool _hasIdentity;
- private DateTime? _lastLoginDateUtc;
- private bool _emailConfirmed;
- private string _name;
- private int _accessFailedCount;
- private string _passwordHash;
- private string _passwordConfig;
- private string _culture;
- private ObservableCollection _logins;
- private Lazy> _getLogins;
- private IReadOnlyUserGroup[] _groups;
- private string[] _allowedSections;
- private int[] _startMediaIds;
- private int[] _startContentIds;
- private DateTime? _lastPasswordChangeDateUtc;
-
- ///
- /// Used to construct a new instance without an identity
- ///
- ///
- /// This is allowed to be null (but would need to be filled in if trying to persist this instance)
- ///
- ///
- public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string username, string email, string culture, string name = null)
- {
- if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
- if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
-
- var user = new BackOfficeIdentityUser(globalSettings, Array.Empty());
- user.DisableChangeTracking();
- user._userName = username;
- user._email = email;
- //we are setting minvalue here because the default is "0" which is the id of the admin user
- //which we cannot allow because the admin user will always exist
- user._id = int.MinValue;
- user._hasIdentity = false;
- user._culture = culture;
- user._name = name;
- user.EnableChangeTracking();
- return user;
- }
-
- private BackOfficeIdentityUser(GlobalSettings globalSettings, IReadOnlyUserGroup[] groups)
- {
- _startMediaIds = Array.Empty();
- _startContentIds = Array.Empty();
- _allowedSections = Array.Empty();
- _culture = globalSettings.DefaultUILanguage;
-
- // must initialize before setting groups
- _roles = new ObservableCollection>();
- _roles.CollectionChanged += _roles_CollectionChanged;
-
- // use the property setters - they do more than just setting a field
- Groups = groups;
- }
-
- ///
- /// Creates an existing user with the specified groups
- ///
- ///
- ///
- ///
- public BackOfficeIdentityUser(GlobalSettings globalSettings, int userId, IEnumerable groups)
- : this(globalSettings, groups.ToArray())
- {
- // use the property setters - they do more than just setting a field
- Id = userId;
- }
-
- ///
- /// Returns true if an Id has been set on this object this will be false if the object is new and not persisted to the database
- ///
- public bool HasIdentity => _hasIdentity;
-
- public int[] CalculatedMediaStartNodeIds { get; set; }
- public int[] CalculatedContentStartNodeIds { get; set; }
-
- public override int Id
- {
- get => _id;
- set
- {
- _id = value;
- _hasIdentity = true;
- }
- }
-
- ///
- /// Override Email so we can track changes to it
- ///
- public override string Email
- {
- get => _email;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email));
- }
-
- ///
- /// Override UserName so we can track changes to it
- ///
- public override string UserName
- {
- get => _userName;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _userName, nameof(UserName));
- }
-
- ///
- /// LastPasswordChangeDateUtc so we can track changes to it
- ///
- public override DateTime? LastPasswordChangeDateUtc
- {
- get { return _lastPasswordChangeDateUtc; }
- set { _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDateUtc, nameof(LastPasswordChangeDateUtc)); }
- }
-
- ///
- /// Override LastLoginDateUtc so we can track changes to it
- ///
- public override DateTime? LastLoginDateUtc
- {
- get => _lastLoginDateUtc;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, nameof(LastLoginDateUtc));
- }
-
- ///
- /// Override EmailConfirmed so we can track changes to it
- ///
- public override bool EmailConfirmed
- {
- get => _emailConfirmed;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, nameof(EmailConfirmed));
- }
-
- ///
- /// Gets/sets the user's real name
- ///
- public string Name
- {
- get => _name;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
- }
-
- ///
- /// Override AccessFailedCount so we can track changes to it
- ///
- public override int AccessFailedCount
- {
- get => _accessFailedCount;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, nameof(AccessFailedCount));
- }
-
- ///
- /// Override PasswordHash so we can track changes to it
- ///
- public override string PasswordHash
- {
- get => _passwordHash;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash));
- }
-
- public string PasswordConfig
- {
- get => _passwordConfig;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
- }
-
-
- ///
- /// Content start nodes assigned to the User (not ones assigned to the user's groups)
- ///
- public int[] StartContentIds
- {
- get => _startContentIds;
- set
- {
- if (value == null) value = new int[0];
- _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), StartIdsComparer);
- }
- }
-
- ///
- /// Media start nodes assigned to the User (not ones assigned to the user's groups)
- ///
- public int[] StartMediaIds
- {
- get => _startMediaIds;
- set
- {
- if (value == null) value = new int[0];
- _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), StartIdsComparer);
- }
- }
-
- ///
- /// This is a readonly list of the user's allowed sections which are based on it's user groups
- ///
- public string[] AllowedSections
- {
- get { return _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray()); }
- }
-
- public string Culture
- {
- get => _culture;
- set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture));
- }
-
- public IReadOnlyUserGroup[] Groups
- {
- get => _groups;
- set
- {
- //so they recalculate
- _allowedSections = null;
-
- _groups = value;
-
- //now clear all roles and re-add them
- _roles.CollectionChanged -= _roles_CollectionChanged;
- _roles.Clear();
- foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole
- {
- RoleId = x.Alias,
- UserId = Id.ToString()
- }))
- {
- _roles.Add(identityUserRole);
- }
- _roles.CollectionChanged += _roles_CollectionChanged;
-
- _beingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), GroupsComparer);
- }
- }
-
- ///
- /// Lockout is always enabled
- ///
- public override bool LockoutEnabled
- {
- get { return true; }
- set
- {
- //do nothing
- }
- }
-
- ///
- /// Based on the user's lockout end date, this will determine if they are locked out
- ///
- public bool IsLockedOut
- {
- get
- {
- var isLocked = LockoutEndDateUtc.HasValue && LockoutEndDateUtc.Value.ToLocalTime() >= DateTime.Now;
- return isLocked;
- }
- }
-
- ///
- /// This is a 1:1 mapping with IUser.IsApproved
- ///
- public bool IsApproved { get; set; }
-
- ///
- /// Overridden to make the retrieval lazy
- ///
- public override ICollection Logins
- {
- get
- {
- // return if it exists
- if (_logins != null) return _logins;
-
- _logins = new ObservableCollection();
-
- // if the callback is there and hasn't been created yet then execute it and populate the logins
- if (_getLogins != null && !_getLogins.IsValueCreated)
- {
- foreach (var l in _getLogins.Value)
- {
- _logins.Add(l);
- }
- }
-
- //now assign events
- _logins.CollectionChanged += Logins_CollectionChanged;
-
- return _logins;
- }
- }
-
- void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
- {
- _beingDirty.OnPropertyChanged(nameof(Logins));
- }
-
- private void _roles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
- {
- _beingDirty.OnPropertyChanged(nameof(Roles));
- }
-
- private readonly ObservableCollection> _roles;
-
- ///
- /// helper method to easily add a role without having to deal with IdentityUserRole{T}
- ///
- ///
- ///
- /// Adding a role this way will not reflect on the user's group's collection or it's allowed sections until the user is persisted
- ///
- public void AddRole(string role)
- {
- Roles.Add(new IdentityUserRole
- {
- UserId = Id.ToString(),
- RoleId = role
- });
- }
-
- ///
- /// Override Roles because the value of these are the user's group aliases
- ///
- public override ICollection> Roles => _roles;
-
- ///
- /// Used to set a lazy call back to populate the user's Login list
- ///
- ///
- public void SetLoginsCallback(Lazy> callback)
- {
- _getLogins = callback ?? throw new ArgumentNullException(nameof(callback));
- }
-
- #region BeingDirty
-
- private readonly BeingDirty _beingDirty = new BeingDirty();
-
- ///
- public bool IsDirty()
- {
- return _beingDirty.IsDirty();
- }
-
- ///
- public bool IsPropertyDirty(string propName)
- {
- return _beingDirty.IsPropertyDirty(propName);
- }
-
- ///
- public IEnumerable GetDirtyProperties()
- {
- return _beingDirty.GetDirtyProperties();
- }
-
- ///
- public void ResetDirtyProperties()
- {
- _beingDirty.ResetDirtyProperties();
- }
-
- ///
- public bool WasDirty()
- {
- return _beingDirty.WasDirty();
- }
-
- ///
- public bool WasPropertyDirty(string propertyName)
- {
- return _beingDirty.WasPropertyDirty(propertyName);
- }
-
- ///
- public void ResetWereDirtyProperties()
- {
- _beingDirty.ResetWereDirtyProperties();
- }
-
- ///
- public void ResetDirtyProperties(bool rememberDirty)
- {
- _beingDirty.ResetDirtyProperties(rememberDirty);
- }
-
- ///
- public IEnumerable GetWereDirtyProperties()
- => _beingDirty.GetWereDirtyProperties();
-
- ///
- /// Disables change tracking.
- ///
- public void DisableChangeTracking()
- {
- _beingDirty.DisableChangeTracking();
- }
-
- ///
- /// Enables change tracking.
- ///
- public void EnableChangeTracking()
- {
- _beingDirty.EnableChangeTracking();
- }
-
- public event PropertyChangedEventHandler PropertyChanged
- {
- add
- {
- _beingDirty.PropertyChanged += value;
- }
- remove
- {
- _beingDirty.PropertyChanged -= value;
- }
- }
-
- #endregion
-
- //Custom comparer for enumerables
- private static readonly DelegateEqualityComparer GroupsComparer = new DelegateEqualityComparer(
- (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)),
- groups => groups.GetHashCode());
-
- private static readonly DelegateEqualityComparer StartIdsComparer = new DelegateEqualityComparer(
- (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable),
- groups => groups.GetHashCode());
-
- }
-}
diff --git a/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs
index 6a5fd8e73f..0c143f22e6 100644
--- a/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs
+++ b/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs
@@ -1,19 +1,49 @@
-namespace Umbraco.Core.Configuration
-{
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+namespace Umbraco.Core.Configuration
+{
///
/// Password configuration
///
public interface IPasswordConfiguration
{
+ ///
+ /// Gets a value for the minimum required length for the password.
+ ///
int RequiredLength { get; }
+
+ ///
+ /// Gets a value indicating whether at least one non-letter or digit is required for the password.
+ ///
bool RequireNonLetterOrDigit { get; }
+
+ ///
+ /// Gets a value indicating whether at least one digit is required for the password.
+ ///
bool RequireDigit { get; }
+
+ ///
+ /// Gets a value indicating whether at least one lower-case character is required for the password.
+ ///
bool RequireLowercase { get; }
+
+ ///
+ /// Gets a value indicating whether at least one upper-case character is required for the password.
+ ///
bool RequireUppercase { get; }
+
+ ///
+ /// Gets a value for the password hash algorithm type.
+ ///
string HashAlgorithmType { get; }
- // TODO: This doesn't really belong here
+ ///
+ /// Gets a value for the maximum failed access attempts before lockout.
+ ///
+ ///
+ /// TODO: This doesn't really belong here
+ ///
int MaxFailedAccessAttemptsBeforeLockout { get; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs b/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs
index 0fe541416a..135b8b763c 100644
--- a/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs
@@ -1,7 +1,16 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for active directory settings.
+ ///
public class ActiveDirectorySettings
{
+ ///
+ /// Gets or sets a value for the Active Directory domain.
+ ///
public string Domain { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs
index d21cba4486..52b8a02478 100644
--- a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs
@@ -1,19 +1,30 @@
-using System.Collections.Generic;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for connection strings.
+ ///
public class ConnectionStrings
{
// Backing field for UmbracoConnectionString to load from configuration value with key umbracoDbDSN.
// Attributes cannot be applied to map from keys that don't match, and have chosen to retain the key name
// used in configuration for older Umbraco versions.
// See: https://stackoverflow.com/a/54607296/489433
+#pragma warning disable SA1300 // Element should begin with upper-case letter
+#pragma warning disable IDE1006 // Naming Styles
private string umbracoDbDSN
+#pragma warning restore IDE1006 // Naming Styles
+#pragma warning restore SA1300 // Element should begin with upper-case letter
{
get => UmbracoConnectionString?.ConnectionString;
set => UmbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, value);
}
+ ///
+ /// Gets or sets a value for the Umbraco database connection string..
+ ///
public ConfigConnectionString UmbracoConnectionString { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs b/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs
index 6a362c93a3..842b2e6fb7 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs
@@ -1,30 +1,55 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.ComponentModel.DataAnnotations;
using Umbraco.Core.Configuration.Models.Validation;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration for a content error page.
+ ///
public class ContentErrorPage : ValidatableEntryBase
{
+ ///
+ /// Gets or sets a value for the content Id.
+ ///
public int ContentId { get; set; }
+ ///
+ /// Gets or sets a value for the content key.
+ ///
public Guid ContentKey { get; set; }
+ ///
+ /// Gets or sets a value for the content XPath.
+ ///
public string ContentXPath { get; set; }
+ ///
+ /// Gets a value indicating whether the field is populated.
+ ///
public bool HasContentId => ContentId != 0;
+ ///
+ /// Gets a value indicating whether the field is populated.
+ ///
public bool HasContentKey => ContentKey != Guid.Empty;
+ ///
+ /// Gets a value indicating whether the field is populated.
+ ///
public bool HasContentXPath => !string.IsNullOrEmpty(ContentXPath);
+ ///
+ /// Gets or sets a value for the content culture.
+ ///
[Required]
public string Culture { get; set; }
- internal override bool IsValid()
- {
- return base.IsValid() &&
+ internal override bool IsValid() =>
+ base.IsValid() &&
((HasContentId ? 1 : 0) + (HasContentKey ? 1 : 0) + (HasContentXPath ? 1 : 0) == 1);
- }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs
index 7c1e570426..d31c5cb6d7 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs
@@ -1,9 +1,15 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for content imaging settings.
+ ///
public class ContentImagingSettings
{
- private static readonly ImagingAutoFillUploadField[] DefaultImagingAutoFillUploadField =
-{
+ private static readonly ImagingAutoFillUploadField[] s_defaultImagingAutoFillUploadField =
+ {
new ImagingAutoFillUploadField
{
Alias = Constants.Conventions.Media.File,
@@ -14,8 +20,14 @@
}
};
+ ///
+ /// Gets or sets a value for the collection of accepted image file extensions.
+ ///
public string[] ImageFileTypes { get; set; } = new[] { "jpeg", "jpg", "gif", "bmp", "png", "tiff", "tif" };
- public ImagingAutoFillUploadField[] AutoFillImageProperties { get; set; } = DefaultImagingAutoFillUploadField;
+ ///
+ /// Gets or sets a value for the imaging autofill following media file upload fields.
+ ///
+ public ImagingAutoFillUploadField[] AutoFillImageProperties { get; set; } = s_defaultImagingAutoFillUploadField;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs
index ab1c10ff77..48a131adfa 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs
@@ -1,9 +1,21 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for content notification settings.
+ ///
public class ContentNotificationSettings
{
+ ///
+ /// Gets or sets a value for the email address for notifications.
+ ///
public string Email { get; set; }
+ ///
+ /// Gets or sets a value indicating whether HTML email notifications should be disabled.
+ ///
public bool DisableHtmlEmail { get; set; } = false;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
index c26e6d403a..55881cd8db 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
@@ -1,10 +1,15 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.Collections.Generic;
-using Umbraco.Core.Configuration.Models.Validation;
using Umbraco.Core.Macros;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for content settings.
+ ///
public class ContentSettings
{
private const string DefaultPreviewBadge =
@@ -142,26 +147,54 @@ namespace Umbraco.Core.Configuration.Models
";
+ ///
+ /// Gets or sets a value for the content notification settings.
+ ///
public ContentNotificationSettings Notifications { get; set; } = new ContentNotificationSettings();
+ ///
+ /// Gets or sets a value for the content imaging settings.
+ ///
public ContentImagingSettings Imaging { get; set; } = new ContentImagingSettings();
+ ///
+ /// Gets or sets a value indicating whether URLs should be resolved from text strings.
+ ///
public bool ResolveUrlsFromTextString { get; set; } = false;
+ ///
+ /// Gets or sets a value for the collection of error pages.
+ ///
public ContentErrorPage[] Error404Collection { get; set; } = Array.Empty();
+ ///
+ /// Gets or sets a value for the preview badge mark-up.
+ ///
public string PreviewBadge { get; set; } = DefaultPreviewBadge;
+ ///
+ /// Gets or sets a value for the macro error behaviour.
+ ///
public MacroErrorBehaviour MacroErrors { get; set; } = MacroErrorBehaviour.Inline;
+ ///
+ /// Gets or sets a value for the collection of file extensions that are disallowed for upload.
+ ///
public IEnumerable DisallowedUploadFiles { get; set; } = new[] { "ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd" };
+ ///
+ /// Gets or sets a value for the collection of file extensions that are allowed for upload.
+ ///
public IEnumerable AllowedUploadFiles { get; set; } = Array.Empty();
+ ///
+ /// Gets or sets a value indicating whether deprecated property editors should be shown.
+ ///
public bool ShowDeprecatedPropertyEditors { get; set; } = false;
+ ///
+ /// Gets or sets a value for the pate to the login screen background image.
+ ///
public string LoginBackgroundImage { get; set; } = "assets/img/login.jpg";
-
-
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs b/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs
index 2b13609509..a263fb648a 100644
--- a/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs
@@ -1,9 +1,21 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for core debug settings.
+ ///
public class CoreDebugSettings
{
- public bool LogUncompletedScopes { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether incompleted scopes should be logged.
+ ///
+ public bool LogIncompletedScopes { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether memory dumps on thread abort should be taken.
+ ///
public bool DumpOnTimeoutThreadAbort { get; set; } = false;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs b/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs
index 32b957d5d0..8ad87bbb4e 100644
--- a/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs
@@ -1,26 +1,32 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for database server messaging settings.
+ ///
public class DatabaseServerMessengerSettings
{
///
- /// The maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches).
+ /// Gets or sets a value for the maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches).
///
public int MaxProcessingInstructionCount { get; set; } = 1000;
///
- /// The time to keep instructions in the database; records older than this number will be pruned.
+ /// Gets or sets a value for the time to keep instructions in the database; records older than this number will be pruned.
///
public TimeSpan TimeToRetainInstructions { get; set; } = TimeSpan.FromDays(2);
///
- /// The time to wait between each sync operations.
+ /// Gets or sets a value for the time to wait between each sync operations.
///
public TimeSpan TimeBetweenSyncOperations { get; set; } = TimeSpan.FromSeconds(5);
///
- /// The time to wait between each prune operations.
+ /// Gets or sets a value for the time to wait between each prune operations.
///
public TimeSpan TimeBetweenPruneOperations { get; set; } = TimeSpan.FromMinutes(1);
}
diff --git a/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs b/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs
index 1aa670fae6..ae2502d8af 100644
--- a/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs
@@ -1,16 +1,22 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for database server registrar settings.
+ ///
public class DatabaseServerRegistrarSettings
{
///
- /// The amount of time to wait between calls to the database on the background thread.
+ /// Gets or sets a value for the amount of time to wait between calls to the database on the background thread.
///
public TimeSpan WaitTimeBetweenCalls { get; set; } = TimeSpan.FromMinutes(1);
///
- /// The time span to wait before considering a server stale, after it has last been accessed.
+ /// Gets or sets a value for the time span to wait before considering a server stale, after it has last been accessed.
///
public TimeSpan StaleServerTimeout { get; set; } = TimeSpan.FromMinutes(2);
}
diff --git a/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs b/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs
index 1d96a9027f..38c71fd83f 100644
--- a/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs
@@ -1,13 +1,28 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for disabled healthcheck settings.
+ ///
public class DisabledHealthCheckSettings
{
+ ///
+ /// Gets or sets a value for the healthcheck Id to disable.
+ ///
public Guid Id { get; set; }
+ ///
+ /// Gets or sets a value for the date the healthcheck was disabled.
+ ///
public DateTime DisabledOn { get; set; }
+ ///
+ /// Gets or sets a value for Id of the user that disabled the healthcheck.
+ ///
public int DisabledBy { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs b/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs
index 6b8f74bef0..1a1362ff21 100644
--- a/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs
@@ -1,7 +1,16 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for exception filter settings.
+ ///
public class ExceptionFilterSettings
{
+ ///
+ /// Gets or sets a value indicating whether the exception filter is disabled.
+ ///
public bool Disabled { get; set; } = false;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
index b6e5cf2b4d..eb13427d2b 100644
--- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
@@ -1,68 +1,141 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
///
- /// The GlobalSettings Class contains general settings information for the entire Umbraco instance based on information
- /// from web.config appsettings
+ /// Typed configuration options for global settings.
///
public class GlobalSettings
{
internal const string
- StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/,"; //must end with a comma!
+ StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/,"; // must end with a comma!
internal const string
- StaticReservedUrls = "~/config/splashes/noNodes.aspx,~/.well-known,"; //must end with a comma!
+ StaticReservedUrls = "~/config/splashes/noNodes.aspx,~/.well-known,"; // must end with a comma!
+ ///
+ /// Gets or sets a value for the reserved URLs.
+ ///
public string ReservedUrls { get; set; } = StaticReservedUrls;
+ ///
+ /// Gets or sets a value for the reserved paths.
+ ///
public string ReservedPaths { get; set; } = StaticReservedPaths;
- // TODO: https://github.com/umbraco/Umbraco-CMS/issues/4238 - stop having version in web.config appSettings
- // TODO: previously this would throw on set, but presumably we can't do that if we do still want this in config.
+ ///
+ /// Gets or sets a value for the configuration status.
+ ///
+ ///
+ /// TODO: https://github.com/umbraco/Umbraco-CMS/issues/4238 - stop having version in web.config appSettings
+ /// TODO: previously this would throw on set, but presumably we can't do that if we do still want this in config.
+ ///
public string ConfigurationStatus { get; set; }
+ ///
+ /// Gets or sets a value for the timeout in minutes.
+ ///
public int TimeOutInMinutes { get; set; } = 20;
+ ///
+ /// Gets or sets a value for the default UI language.
+ ///
public string DefaultUILanguage { get; set; } = "en-US";
+ ///
+ /// Gets or sets a value indicating whether to hide the top level node from the path.
+ ///
public bool HideTopLevelNodeFromPath { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether HTTPS should be used.
+ ///
public bool UseHttps { get; set; } = false;
+ ///
+ /// Gets or sets a value for the version check period in days.
+ ///
public int VersionCheckPeriod { get; set; } = 7;
+ ///
+ /// Gets or sets a value for the Umbraco back-office path.
+ ///
public string UmbracoPath { get; set; } = "~/umbraco";
- // TODO: Umbraco cannot be hard coded here that is what UmbracoPath is for
- // so this should not be a normal get set it has to have dynamic ability to return the correct
- // path given UmbracoPath if this hasn't been explicitly set.
+ ///
+ /// Gets or sets a value for the Umbraco icons path.
+ ///
+ ///
+ /// TODO: Umbraco cannot be hard coded here that is what UmbracoPath is for
+ /// so this should not be a normal get set it has to have dynamic ability to return the correct
+ /// path given UmbracoPath if this hasn't been explicitly set.
+ ///
public string IconsPath { get; set; } = $"~/umbraco/assets/icons";
+ ///
+ /// Gets or sets a value for the Umbraco CSS path.
+ ///
public string UmbracoCssPath { get; set; } = "~/css";
+ ///
+ /// Gets or sets a value for the Umbraco scripts path.
+ ///
public string UmbracoScriptsPath { get; set; } = "~/scripts";
+ ///
+ /// Gets or sets a value for the Umbraco media path.
+ ///
public string UmbracoMediaPath { get; set; } = "~/media";
+ ///
+ /// Gets or sets a value indicating whether to install the database when it is missing.
+ ///
public bool InstallMissingDatabase { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether to install the database when it is empty.
+ ///
public bool InstallEmptyDatabase { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether to disable the election for a single server.
+ ///
public bool DisableElectionForSingleServer { get; set; } = false;
- public DatabaseServerRegistrarSettings DatabaseServerRegistrar { get; set; } = new DatabaseServerRegistrarSettings();
-
- public DatabaseServerMessengerSettings DatabaseServerMessenger { get; set; } = new DatabaseServerMessengerSettings();
-
- public string RegisterType { get; set; } = string.Empty;
-
+ ///
+ /// Gets or sets a value for the database factory server version.
+ ///
public string DatabaseFactoryServerVersion { get; set; } = string.Empty;
+ ///
+ /// Gets or sets a value for the main dom lock.
+ ///
public string MainDomLock { get; set; } = string.Empty;
+ ///
+ /// Gets or sets a value for the path to the no content view.
+ ///
public string NoNodesViewPath { get; set; } = "~/umbraco/UmbracoWebsite/NoNodes.cshtml";
- public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host);
+ ///
+ /// Gets or sets a value for the database server registrar settings.
+ ///
+ public DatabaseServerRegistrarSettings DatabaseServerRegistrar { get; set; } = new DatabaseServerRegistrarSettings();
+ ///
+ /// Gets or sets a value for the database server messenger settings.
+ ///
+ public DatabaseServerMessengerSettings DatabaseServerMessenger { get; set; } = new DatabaseServerMessengerSettings();
+
+ ///
+ /// Gets or sets a value for the SMTP settings.
+ ///
public SmtpSettings Smtp { get; set; }
+
+ ///
+ /// Gets a value indicating whether SMTP is configured.
+ ///
+ public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host);
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs
index 1cfc4f9168..b4bffab4d6 100644
--- a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs
@@ -1,16 +1,34 @@
-using System.Collections.Generic;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Collections.Generic;
using Umbraco.Core.HealthCheck;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for healthcheck notification method settings.
+ ///
public class HealthChecksNotificationMethodSettings
{
+ ///
+ /// Gets or sets a value indicating whether the health check notification method is enabled.
+ ///
public bool Enabled { get; set; } = false;
+ ///
+ /// Gets or sets a value for the health check notifications reporting verbosity.
+ ///
public HealthCheckNotificationVerbosity Verbosity { get; set; } = HealthCheckNotificationVerbosity.Summary;
+ ///
+ /// Gets or sets a value indicating whether the health check notifications should occur on failures only.
+ ///
public bool FailureOnly { get; set; } = false;
+ ///
+ /// Gets or sets a value providing provider specific settings for the health check notification method.
+ ///
public IDictionary Settings { get; set; } = new Dictionary();
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs
index 052b5a4997..3e52a70b29 100644
--- a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs
@@ -1,19 +1,40 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for healthcheck notification settings.
+ ///
public class HealthChecksNotificationSettings
{
+ ///
+ /// Gets or sets a value indicating whether health check notifications are enabled.
+ ///
public bool Enabled { get; set; } = false;
+ ///
+ /// Gets or sets a value for the first run time of a healthcheck notification in crontab format.
+ ///
public string FirstRunTime { get; set; } = string.Empty;
+ ///
+ /// Gets or sets a value for the period of the healthcheck notification.
+ ///
public TimeSpan Period { get; set; } = TimeSpan.FromHours(24);
+ ///
+ /// Gets or sets a value for the collection of health check notification methods.
+ ///
public IDictionary NotificationMethods { get; set; } = new Dictionary();
+ ///
+ /// Gets or sets a value for the collection of health checks that are disabled for notifications.
+ ///
public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty();
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs
index 705611b0c1..e3ae9a3f96 100644
--- a/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs
@@ -1,12 +1,24 @@
-using System.Collections.Generic;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for healthchecks settings.
+ ///
public class HealthChecksSettings
{
+ ///
+ /// Gets or sets a value for the collection of healthchecks that are disabled.
+ ///
public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty();
+ ///
+ /// Gets or sets a value for the healthcheck notification settings.
+ ///
public HealthChecksNotificationSettings Notification { get; set; } = new HealthChecksNotificationSettings();
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs
index b003315c56..0478e9b7e1 100644
--- a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs
@@ -1,18 +1,25 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for hosting settings.
+ ///
public class HostingSettings
{
+ ///
+ /// Gets or sets a value for the application virtual path.
+ ///
public string ApplicationVirtualPath { get; set; }
///
- /// Gets the configuration for the location of temporary files.
+ /// Gets or sets a value for the location of temporary files.
///
public LocalTempStorage LocalTempStorageLocation { get; set; } = LocalTempStorage.Default;
///
- /// Gets a value indicating whether umbraco is running in [debug mode].
+ /// Gets or sets a value indicating whether umbraco is running in [debug mode].
///
/// true if [debug mode]; otherwise, false.
public bool Debug { get; set; } = false;
diff --git a/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs b/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs
index f58e2bb4f8..999bcf2dff 100644
--- a/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs
+++ b/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs
@@ -1,22 +1,43 @@
-using System.ComponentModel.DataAnnotations;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.ComponentModel.DataAnnotations;
using Umbraco.Core.Configuration.Models.Validation;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for image autofill upload settings.
+ ///
public class ImagingAutoFillUploadField : ValidatableEntryBase
{
+ ///
+ /// Gets or sets a value for the alias of the image upload property.
+ ///
[Required]
public string Alias { get; set; }
+ ///
+ /// Gets or sets a value for the width field alias of the image upload property.
+ ///
[Required]
public string WidthFieldAlias { get; set; }
+ ///
+ /// Gets or sets a value for the height field alias of the image upload property.
+ ///
[Required]
public string HeightFieldAlias { get; set; }
+ ///
+ /// Gets or sets a value for the length field alias of the image upload property.
+ ///
[Required]
public string LengthFieldAlias { get; set; }
+ ///
+ /// Gets or sets a value for the extension field alias of the image upload property.
+ ///
[Required]
public string ExtensionFieldAlias { get; set; }
}
diff --git a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs
index 44f5ae89b6..0a3e723722 100644
--- a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs
@@ -1,17 +1,34 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.IO;
-using System.Threading;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for image cache settings.
+ ///
public class ImagingCacheSettings
{
- public TimeSpan BrowserMaxAge { get; set; } = TimeSpan.FromDays(7);
+ ///
+ /// Gets or sets a value for the browser image cache maximum age.
+ ///
+ public TimeSpan BrowserMaxAge { get; set; } = TimeSpan.FromDays(7);
+ ///
+ /// Gets or sets a value for the image cache maximum age.
+ ///
public TimeSpan CacheMaxAge { get; set; } = TimeSpan.FromDays(365);
+ ///
+ /// Gets or sets a value for length of the cached name.
+ ///
public uint CachedNameLength { get; set; } = 8;
+ ///
+ /// Gets or sets a value for the cache folder.
+ ///
public string CacheFolder { get; set; } = Path.Combine("..", "umbraco", "mediacache");
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs
index f9db53d7dd..c95aad52ad 100644
--- a/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs
@@ -1,9 +1,21 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for image resize settings.
+ ///
public class ImagingResizeSettings
{
+ ///
+ /// Gets or sets a value for the maximim resize width.
+ ///
public int MaxWidth { get; set; } = 5000;
+ ///
+ /// Gets or sets a value for the maximim resize height.
+ ///
public int MaxHeight { get; set; } = 5000;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs
index 2f253b151b..343e2a040f 100644
--- a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs
@@ -1,9 +1,21 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for imaging settings.
+ ///
public class ImagingSettings
{
+ ///
+ /// Gets or sets a value for imaging cache settings.
+ ///
public ImagingCacheSettings Cache { get; set; } = new ImagingCacheSettings();
+ ///
+ /// Gets or sets a value for imaging resize settings.
+ ///
public ImagingResizeSettings Resize { get; set; } = new ImagingResizeSettings();
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs b/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs
index fcc22de9a3..9ea7348211 100644
--- a/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs
@@ -1,7 +1,16 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for index creator settings.
+ ///
public class IndexCreatorSettings
{
+ ///
+ /// Gets or sets a value for lucene directory factory type.
+ ///
public string LuceneDirectoryFactory { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs
index 2c53407398..57336d35ac 100644
--- a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs
@@ -1,9 +1,21 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for keep alive settings.
+ ///
public class KeepAliveSettings
{
+ ///
+ /// Gets or sets a value indicating whether the keep alive task is disabled.
+ ///
public bool DisableKeepAliveTask { get; set; } = false;
+ ///
+ /// Gets a value for the keep alive ping URL.
+ ///
public string KeepAlivePingUrl => "{umbracoApplicationUrl}/api/keepalive/ping";
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs
index 7d79bb83ae..49f2517f8f 100644
--- a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs
@@ -1,9 +1,18 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for logging settings.
+ ///
public class LoggingSettings
{
+ ///
+ /// Gets or sets a value for the maximum age of a log file.
+ ///
public TimeSpan MaxLogAge { get; set; } = TimeSpan.FromHours(24);
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs b/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs
index 52bba6f4b8..abd12ae023 100644
--- a/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs
@@ -1,19 +1,32 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for member password settings.
+ ///
public class MemberPasswordConfigurationSettings : IPasswordConfiguration
{
+ ///
public int RequiredLength { get; set; } = 10;
+ ///
public bool RequireNonLetterOrDigit { get; set; } = false;
+ ///
public bool RequireDigit { get; set; } = false;
+ ///
public bool RequireLowercase { get; set; } = false;
+ ///
public bool RequireUppercase { get; set; } = false;
+ ///
public string HashAlgorithmType { get; set; } = Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName;
+ ///
public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = 5;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
index f0b56561e2..bace0f96c4 100644
--- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
@@ -1,17 +1,21 @@
-using Umbraco.Configuration;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Configuration;
namespace Umbraco.Core.Configuration.Models
{
///
- /// Represents the models builder configuration.
+ /// Typed configuration options for models builder settings.
///
public class ModelsBuilderSettings
{
- // TODO: This should not go into App_Data - that folder isn't really a real thing anymore
- public static string DefaultModelsDirectory => "~/umbraco/models";
+ private bool _flagOutOfDateModels;
+
+ private static string DefaultModelsDirectory => "~/umbraco/models";
///
- /// Gets a value indicating whether the whole models experience is enabled.
+ /// Gets or sets a value indicating whether the whole models experience is enabled.
///
///
/// If this is false then absolutely nothing happens.
@@ -20,31 +24,29 @@ namespace Umbraco.Core.Configuration.Models
public bool Enable { get; set; } = false;
///
- /// Gets the models mode.
+ /// Gets or sets a value for the models mode.
///
public ModelsMode ModelsMode { get; set; } = ModelsMode.Nothing;
///
- /// Gets the models namespace.
+ /// Gets or sets a value for models namespace.
///
/// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied.
public string ModelsNamespace { get; set; }
///
- /// Gets a value indicating whether we should enable the models factory.
+ /// Gets or sets a value indicating whether we should enable the models factory.
///
/// Default value is true because no factory is enabled by default in Umbraco.
public bool EnableFactory { get; set; } = true;
- private bool _flagOutOfDateModels;
-
///
- /// Gets a value indicating whether we should flag out-of-date models.
+ /// Gets or sets a value indicating whether we should flag out-of-date models.
///
///
- /// Models become out-of-date when data types or content types are updated. When this
- /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are
- /// generated through the dashboard, the files is cleared. Default value is false.
+ /// Models become out-of-date when data types or content types are updated. When this
+ /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are
+ /// generated through the dashboard, the files is cleared. Default value is false.
///
public bool FlagOutOfDateModels
{
@@ -62,22 +64,22 @@ namespace Umbraco.Core.Configuration.Models
}
///
- /// Gets the models directory.
+ /// Gets or sets a value for the models directory.
///
/// Default is ~/App_Data/Models but that can be changed.
public string ModelsDirectory { get; set; } = DefaultModelsDirectory;
///
- /// Gets a value indicating whether to accept an unsafe value for ModelsDirectory.
+ /// Gets or sets a value indicating whether to accept an unsafe value for ModelsDirectory.
///
///
- /// An unsafe value is an absolute path, or a relative path pointing outside
- /// of the website root.
+ /// An unsafe value is an absolute path, or a relative path pointing outside
+ /// of the website root.
///
public bool AcceptUnsafeModelsDirectory { get; set; } = false;
///
- /// Gets a value indicating the debug log level.
+ /// Gets or sets a value indicating the debug log level.
///
/// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe).
public int DebugLevel { get; set; } = 0;
diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
index 89a726f30a..f98b1f422e 100644
--- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
@@ -1,7 +1,16 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
- public class NuCacheSettings
+ ///
+ /// Typed configuration options for NuCache settings.
+ ///
+ public class NuCacheSettings
{
+ ///
+ /// Gets or sets a value defining the BTree block size.
+ ///
public int? BTreeBlockSize { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
index d7203b4901..ceea0f9038 100644
--- a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
@@ -1,26 +1,32 @@
-using System.Collections.Generic;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Collections.Generic;
using Umbraco.Core.Configuration.UmbracoSettings;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for request handler settings.
+ ///
public class RequestHandlerSettings
{
internal static readonly CharItem[] DefaultCharCollection =
{
new CharItem { Char = " ", Replacement = "-" },
- new CharItem { Char = "\"", Replacement = "" },
- new CharItem { Char = "'", Replacement = "" },
- new CharItem { Char = "%", Replacement = "" },
- new CharItem { Char = ".", Replacement = "" },
- new CharItem { Char = ";", Replacement = "" },
- new CharItem { Char = "/", Replacement = "" },
- new CharItem { Char = "\\", Replacement = "" },
- new CharItem { Char = ":", Replacement = "" },
- 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 = "" },
- new CharItem { Char = "?", Replacement = "" },
+ 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" },
@@ -29,42 +35,63 @@ namespace Umbraco.Core.Configuration.Models
new CharItem { Char = "ü", Replacement = "ue" },
new CharItem { Char = "ß", Replacement = "ss" },
new CharItem { Char = "|", Replacement = "-" },
- new CharItem { Char = "<", Replacement = "" },
- new CharItem { Char = ">", Replacement = "" }
+ new CharItem { Char = "<", Replacement = string.Empty },
+ new CharItem { Char = ">", Replacement = string.Empty }
};
+ ///
+ /// Gets or sets a value indicating whether to add a trailing slash to URLs.
+ ///
public bool AddTrailingSlash { get; set; } = true;
+ ///
+ /// Gets or sets a value indicating whether to convert URLs to ASCII (valid values: "true", "try" or "false").
+ ///
public string ConvertUrlsToAscii { get; set; } = "try";
+ ///
+ /// Gets a value indicating whether URLs should be converted to ASCII.
+ ///
public bool ShouldConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("true");
+ ///
+ /// Gets a value indicating whether URLs should be tried to be converted to ASCII.
+ ///
public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try");
- //We need to special handle ":", as this character is special in keys
+ // 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();
+ //// 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;
- //}
+ //// if (collection.Any() || _configuration.GetSection("Prefix").GetChildren().Any(x =>
+ //// x.Key.Equals("CharCollection", StringComparison.OrdinalIgnoreCase)))
+ //// {
+ //// return collection;
+ //// }
- // return DefaultCharCollection;
+ //// return DefaultCharCollection;
+
+ ///
+ /// Gets or sets a value for the default character collection for replacements.
+ ///
public IEnumerable CharCollection { get; set; } = DefaultCharCollection;
+ ///
+ /// Defines a character replacement.
+ ///
public class CharItem : IChar
{
+ ///
public string Char { get; set; }
+ ///
public string Replacement { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs
index f93530b490..97af22ea8a 100644
--- a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs
@@ -1,9 +1,21 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for runtime settings.
+ ///
public class RuntimeSettings
{
+ ///
+ /// Gets or sets a value for the maximum query string length.
+ ///
public int? MaxQueryStringLength { get; set; }
+ ///
+ /// Gets or sets a value for the maximum request length.
+ ///
public int? MaxRequestLength { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs
index 55bd3a5bf7..25bbbb645d 100644
--- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs
@@ -1,21 +1,51 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for security settings.
+ ///
public class SecuritySettings
{
+ ///
+ /// Gets or sets a value indicating whether to keep the user logged in.
+ ///
public bool KeepUserLoggedIn { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether to hide disabled users in the back-office.
+ ///
public bool HideDisabledUsersInBackOffice { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether to allow user password reset.
+ ///
public bool AllowPasswordReset { get; set; } = true;
+ ///
+ /// Gets or sets a value for the authorization cookie name.
+ ///
public string AuthCookieName { get; set; } = "UMB_UCONTEXT";
+ ///
+ /// Gets or sets a value for the authorization cookie domain.
+ ///
public string AuthCookieDomain { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the user's email address is to be considered as their username.
+ ///
public bool UsernameIsEmail { get; set; } = true;
+ ///
+ /// Gets or sets a value for the user password settings.
+ ///
public UserPasswordConfigurationSettings UserPassword { get; set; }
+ ///
+ /// Gets or sets a value for the member password settings.
+ ///
public MemberPasswordConfigurationSettings MemberPassword { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs
index c1ba5edea2..fb4462f76d 100644
--- a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs
@@ -1,4 +1,7 @@
-using System.ComponentModel.DataAnnotations;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.ComponentModel.DataAnnotations;
using System.Net.Mail;
using Umbraco.Core.Configuration.Models.Validation;
@@ -6,35 +9,82 @@ namespace Umbraco.Core.Configuration.Models
{
///
/// Matches MailKit.Security.SecureSocketOptions and defined locally to avoid having to take
- /// thi
+ /// a dependency on this external library into Umbraco.Core.
+ /// See: http://www.mimekit.net/docs/html/T_MailKit_Security_SecureSocketOptions.htm
///
public enum SecureSocketOptions
{
+ ///
+ /// No SSL or TLS encryption should be used.
+ ///
None = 0,
+
+ ///
+ /// Allow the IMailService to decide which SSL or TLS options to use (default). If the server does not support SSL or TLS, then the connection will continue without any encryption.
+ ///
Auto = 1,
+
+ ///
+ /// The connection should use SSL or TLS encryption immediately.
+ ///
SslOnConnect = 2,
+
+ ///
+ /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the server. If the server does not support the STARTTLS extension, then the connection will fail and a NotSupportedException will be thrown.
+ ///
StartTls = 3,
+
+ ///
+ /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the server, but only if the server supports the STARTTLS extension.
+ ///
StartTlsWhenAvailable = 4
}
+ ///
+ /// Typed configuration options for SMTP settings.
+ ///
public class SmtpSettings : ValidatableEntryBase
{
+ ///
+ /// Gets or sets a value for the SMTP from address to use for messages.
+ ///
[Required]
[EmailAddress]
public string From { get; set; }
+ ///
+ /// Gets or sets a value for the SMTP host.
+ ///
public string Host { get; set; }
+ ///
+ /// Gets or sets a value for the SMTP port.
+ ///
public int Port { get; set; }
+ ///
+ /// Gets or sets a value for the secure socket options.
+ ///
public SecureSocketOptions SecureSocketOptions { get; set; } = SecureSocketOptions.Auto;
+ ///
+ /// Gets or sets a value for the SMTP pick-up directory.
+ ///
public string PickupDirectoryLocation { get; set; }
+ ///
+ /// Gets or sets a value for the SMTP delivery method.
+ ///
public SmtpDeliveryMethod DeliveryMethod { get; set; } = SmtpDeliveryMethod.Network;
+ ///
+ /// Gets or sets a value for the SMTP user name.
+ ///
public string Username { get; set; }
+ ///
+ /// Gets or sets a value for the SMTP password.
+ ///
public string Password { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/TourSettings.cs b/src/Umbraco.Core/Configuration/Models/TourSettings.cs
index 895eff6dee..25c06b9975 100644
--- a/src/Umbraco.Core/Configuration/Models/TourSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/TourSettings.cs
@@ -1,7 +1,16 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for tour settings.
+ ///
public class TourSettings
{
+ ///
+ /// Gets or sets a value indicating whether back-office tours are enabled.
+ ///
public bool EnableTours { get; set; } = true;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs b/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs
index c5210f6c8e..63295c7259 100644
--- a/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs
@@ -1,7 +1,16 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for type finder settings.
+ ///
public class TypeFinderSettings
{
+ ///
+ /// Gets or sets a value for the assemblies that accept load exceptions during type finder operations.
+ ///
public string AssembliesAcceptingLoadExceptions { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs b/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs
index 445a0f545c..09b9200760 100644
--- a/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs
@@ -1,19 +1,32 @@
-namespace Umbraco.Core.Configuration.Models
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for user password settings.
+ ///
public class UserPasswordConfigurationSettings : IPasswordConfiguration
{
+ ///
public int RequiredLength { get; set; } = 10;
+ ///
public bool RequireNonLetterOrDigit { get; set; } = false;
+ ///
public bool RequireDigit { get; set; } = false;
+ ///
public bool RequireLowercase { get; set; } = false;
+ ///
public bool RequireUppercase { get; set; } = false;
+ ///
public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName;
+ ///
public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = 5;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs b/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs
index 6c0af9802b..348c809a91 100644
--- a/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs
+++ b/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs
@@ -1,11 +1,24 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Core.Configuration.Models.Validation
{
+ ///
+ /// Base class for configuration validators.
+ ///
public abstract class ConfigurationValidatorBase
{
+ ///
+ /// Validates that a string is one of a set of valid values.
+ ///
+ /// Configuration path from where the setting is found.
+ /// The value to check.
+ /// The set of valid values.
+ /// A message to output if the value does not match.
+ /// True if valid, false if not.
public bool ValidateStringIsOneOfValidValues(string configPath, string value, IEnumerable validValues, out string message)
{
if (!validValues.InvariantContains(value))
@@ -18,6 +31,14 @@ namespace Umbraco.Core.Configuration.Models.Validation
return true;
}
+ ///
+ /// Validates that a collection of objects are all valid based on their data annotations.
+ ///
+ /// Configuration path from where the setting is found.
+ /// The values to check.
+ /// Description of validation appended to message if validation fails.
+ /// A message to output if the value does not match.
+ /// True if valid, false if not.
public bool ValidateCollection(string configPath, IEnumerable values, string validationDescription, out string message)
{
if (values.Any(x => !x.IsValid()))
@@ -30,6 +51,14 @@ namespace Umbraco.Core.Configuration.Models.Validation
return true;
}
+ ///
+ /// Validates a configuration entry is valid if provided.
+ ///
+ /// Configuration path from where the setting is found.
+ /// The value to check.
+ /// Description of validation appended to message if validation fails.
+ /// A message to output if the value does not match.
+ /// True if valid, false if not.
public bool ValidateOptionalEntry(string configPath, ValidatableEntryBase value, string validationDescription, out string message)
{
if (value != null && !value.IsValid())
@@ -41,8 +70,5 @@ namespace Umbraco.Core.Configuration.Models.Validation
message = string.Empty;
return true;
}
-
-
-
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs
index f6e839aa39..fdfd6de59c 100644
--- a/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs
+++ b/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs
@@ -1,14 +1,20 @@
-using System.Collections.Generic;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Collections.Generic;
using Microsoft.Extensions.Options;
namespace Umbraco.Core.Configuration.Models.Validation
{
+ ///
+ /// Validator for configuration representated as .
+ ///
public class ContentSettingsValidator : ConfigurationValidatorBase, IValidateOptions
{
+ ///
public ValidateOptionsResult Validate(string name, ContentSettings options)
{
- string message;
- if (!ValidateError404Collection(options.Error404Collection, out message))
+ if (!ValidateError404Collection(options.Error404Collection, out string message))
{
return ValidateOptionsResult.Fail(message);
}
@@ -21,14 +27,10 @@ namespace Umbraco.Core.Configuration.Models.Validation
return ValidateOptionsResult.Success;
}
- private bool ValidateError404Collection(IEnumerable values, out string message)
- {
- return ValidateCollection($"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Error404Collection)}", values, "Culture and one and only one of ContentId, ContentKey and ContentXPath must be specified for each entry", out message);
- }
+ private bool ValidateError404Collection(IEnumerable values, out string message) =>
+ ValidateCollection($"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Error404Collection)}", values, "Culture and one and only one of ContentId, ContentKey and ContentXPath must be specified for each entry", out message);
- private bool ValidateAutoFillImageProperties(IEnumerable values, out string message)
- {
- return ValidateCollection($"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Imaging)}:{nameof(ContentSettings.Imaging.AutoFillImageProperties)}", values, "Alias, WidthFieldAlias, HeightFieldAlias, LengthFieldAlias and ExtensionFieldAlias must be specified for each entry", out message);
- }
+ private bool ValidateAutoFillImageProperties(IEnumerable values, out string message) =>
+ ValidateCollection($"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Imaging)}:{nameof(ContentSettings.Imaging.AutoFillImageProperties)}", values, "Alias, WidthFieldAlias, HeightFieldAlias, LengthFieldAlias and ExtensionFieldAlias must be specified for each entry", out message);
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs
index ca3fee7999..6bc9dc0a6f 100644
--- a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs
+++ b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs
@@ -1,10 +1,17 @@
-using Microsoft.Extensions.Options;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Options;
namespace Umbraco.Core.Configuration.Models.Validation
{
+ ///
+ /// Validator for configuration representated as .
+ ///
public class GlobalSettingsValidator
: ConfigurationValidatorBase, IValidateOptions
{
+ ///
public ValidateOptionsResult Validate(string name, GlobalSettings options)
{
if (!ValidateSmtpSetting(options.Smtp, out var message))
@@ -15,9 +22,7 @@ namespace Umbraco.Core.Configuration.Models.Validation
return ValidateOptionsResult.Success;
}
- private bool ValidateSmtpSetting(SmtpSettings value, out string message)
- {
- return ValidateOptionalEntry($"{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.Smtp)}", value, "A valid From email address is required", out message);
- }
+ private bool ValidateSmtpSetting(SmtpSettings value, out string message) =>
+ ValidateOptionalEntry($"{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.Smtp)}", value, "A valid From email address is required", out message);
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs
index f26fce9bd9..449415c37f 100644
--- a/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs
+++ b/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs
@@ -1,16 +1,24 @@
-using Microsoft.Extensions.Options;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Options;
namespace Umbraco.Core.Configuration.Models.Validation
{
+ ///
+ /// Validator for configuration representated as .
+ ///
public class HealthChecksSettingsValidator : ConfigurationValidatorBase, IValidateOptions
{
private readonly ICronTabParser _cronTabParser;
- public HealthChecksSettingsValidator(ICronTabParser cronTabParser)
- {
- _cronTabParser = cronTabParser;
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Helper for parsing crontab expressions.
+ public HealthChecksSettingsValidator(ICronTabParser cronTabParser) => _cronTabParser = cronTabParser;
+ ///
public ValidateOptionsResult Validate(string name, HealthChecksSettings options)
{
if (!ValidateNotificationFirstRunTime(options.Notification.FirstRunTime, out var message))
@@ -21,12 +29,10 @@ namespace Umbraco.Core.Configuration.Models.Validation
return ValidateOptionsResult.Success;
}
- private bool ValidateNotificationFirstRunTime(string value, out string message)
- {
- return ValidateOptionalCronTab($"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", value, out message);
- }
+ private bool ValidateNotificationFirstRunTime(string value, out string message) =>
+ ValidateOptionalCronTab($"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", value, out message);
- public bool ValidateOptionalCronTab(string configPath, string value, out string message)
+ private bool ValidateOptionalCronTab(string configPath, string value, out string message)
{
if (!string.IsNullOrEmpty(value) && !_cronTabParser.IsValidCronTab(value))
{
diff --git a/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs
index 305fe812f8..647c7438c6 100644
--- a/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs
+++ b/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs
@@ -1,9 +1,16 @@
-using Microsoft.Extensions.Options;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Options;
namespace Umbraco.Core.Configuration.Models.Validation
{
+ ///
+ /// Validator for configuration representated as .
+ ///
public class RequestHandlerSettingsValidator : ConfigurationValidatorBase, IValidateOptions
{
+ ///
public ValidateOptionsResult Validate(string name, RequestHandlerSettings options)
{
if (!ValidateConvertUrlsToAscii(options.ConvertUrlsToAscii, out var message))
diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs b/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs
index 32e3c3270b..37380eb9a6 100644
--- a/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs
+++ b/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs
@@ -1,8 +1,14 @@
-using System.Collections.Generic;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Core.Configuration.Models.Validation
{
+ ///
+ /// Provides a base class for configuration models that can be validated based on data annotations.
+ ///
public abstract class ValidatableEntryBase
{
internal virtual bool IsValid()
diff --git a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
index 476d58c913..9f06046452 100644
--- a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
@@ -1,23 +1,53 @@
-using Umbraco.Core.Models.PublishedContent;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core.Configuration.Models
{
+ ///
+ /// Typed configuration options for web routing settings.
+ ///
public class WebRoutingSettings
{
+ ///
+ /// Gets or sets a value indicating whether IIS custom errors should be skipped.
+ ///
public bool TrySkipIisCustomErrors { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether an internal redirect should preserve the template.
+ ///
public bool InternalRedirectPreservesTemplate { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether the use of alternative templates are disabled.
+ ///
public bool DisableAlternativeTemplates { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether the use of alternative templates should be validated.
+ ///
public bool ValidateAlternativeTemplates { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether find content ID by path is disabled.
+ ///
public bool DisableFindContentByIdPath { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether redirect URL tracking is disabled.
+ ///
public bool DisableRedirectUrlTracking { get; set; } = false;
+ ///
+ /// Gets or sets a value for the URL provider mode ().
+ ///
public UrlMode UrlProviderMode { get; set; } = UrlMode.Auto;
+ ///
+ /// Gets or sets a value for the Umbraco application URL.
+ ///
public string UmbracoApplicationUrl { get; set; }
}
}
diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs
index 24b8b20731..d50e5390d2 100644
--- a/src/Umbraco.Core/Constants-Security.cs
+++ b/src/Umbraco.Core/Constants-Security.cs
@@ -1,4 +1,4 @@
-namespace Umbraco.Core
+namespace Umbraco.Core
{
public static partial class Constants
{
@@ -11,6 +11,8 @@
///
public const int SuperUserId = -1;
+ public const string SuperUserIdAsString = "-1";
+
///
/// The id for the 'unknown' user.
///
@@ -22,7 +24,7 @@
///
/// The name of the 'unknown' user.
///
- public const string UnknownUserName = "SYTEM";
+ public const string UnknownUserName = "SYSTEM";
public const string AdminGroupAlias = "admin";
public const string EditorGroupAlias = "editor";
@@ -38,7 +40,6 @@
public const string BackOfficeTwoFactorRememberMeAuthenticationType = "UmbracoTwoFactorRememberMeCookie";
public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__";
- public const string ForceReAuthFlag = "umbraco-force-auth";
///
/// The prefix used for external identity providers for their authentication type
diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs
index 92e9156f2f..c8151d076c 100644
--- a/src/Umbraco.Core/IO/SystemFiles.cs
+++ b/src/Umbraco.Core/IO/SystemFiles.cs
@@ -9,6 +9,8 @@ namespace Umbraco.Core.IO
{
public static string TinyMceConfig => Constants.SystemDirectories.Config + "/tinyMceConfig.config";
+ public static string TelemetricsIdentifier => Constants.SystemDirectories.Data + "/telemetrics-id.umb";
+
// TODO: Kill this off we don't have umbraco.config XML cache we now have NuCache
public static string GetContentCacheXml(IHostingEnvironment hostingEnvironment)
{
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs
index 39a4718dd0..9a07d29a02 100644
--- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs
@@ -33,6 +33,9 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "hideLabel")]
public bool HideLabel { get; set; }
+ [DataMember(Name = "labelOnTop")]
+ public bool LabelOnTop { get; set; }
+
[DataMember(Name = "validation")]
public PropertyTypeValidation Validation { get; set; }
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs
index 865a10d5d5..2bf1603bb2 100644
--- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs
@@ -15,6 +15,8 @@ namespace Umbraco.Web.Models.ContentEditing
public bool IsRequired { get; set; }
+ public bool LabelOnTop { get; set; }
+
public string IsRequiredMessage { get; set; }
public string ValidationRegExp { get; set; }
diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs
index 4252a29567..793e4e391d 100644
--- a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs
@@ -65,5 +65,8 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "allowSegmentVariant")]
public bool AllowSegmentVariant { get; set; }
+
+ [DataMember(Name = "labelOnTop")]
+ public bool LabelOnTop { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/IPropertyType.cs b/src/Umbraco.Core/Models/IPropertyType.cs
index 288a3f2243..be52339d65 100644
--- a/src/Umbraco.Core/Models/IPropertyType.cs
+++ b/src/Umbraco.Core/Models/IPropertyType.cs
@@ -50,6 +50,11 @@ namespace Umbraco.Core.Models
///
bool Mandatory { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the label of this property type should be displayed on top.
+ ///
+ bool LabelOnTop { get; set; }
+
///
/// Gets of sets the sort order of the property type.
///
diff --git a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs b/src/Umbraco.Core/Models/Identity/ExternalLogin.cs
index 6e4abf2906..a5de9da0cb 100644
--- a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs
+++ b/src/Umbraco.Core/Models/Identity/ExternalLogin.cs
@@ -1,10 +1,13 @@
-using System;
+using System;
namespace Umbraco.Core.Models.Identity
{
///
public class ExternalLogin : IExternalLogin
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public ExternalLogin(string loginProvider, string providerKey, string userData = null)
{
LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider));
diff --git a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs b/src/Umbraco.Core/Models/Identity/IExternalLogin.cs
index 68f66a5cee..2718802324 100644
--- a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs
+++ b/src/Umbraco.Core/Models/Identity/IExternalLogin.cs
@@ -1,12 +1,23 @@
-namespace Umbraco.Core.Models.Identity
+namespace Umbraco.Core.Models.Identity
{
///
/// Used to persist external login data for a user
///
public interface IExternalLogin
{
+ ///
+ /// Gets the login provider
+ ///
string LoginProvider { get; }
+
+ ///
+ /// Gets the provider key
+ ///
string ProviderKey { get; }
+
+ ///
+ /// Gets the user data
+ ///
string UserData { get; }
}
}
diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs
index cbe5b47b38..05703a1b2c 100644
--- a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs
+++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs
@@ -1,27 +1,30 @@
-using Umbraco.Core.Models.Entities;
+using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models.Identity
{
-
+ ///
+ /// An external login provider linked to a user
+ ///
+ /// The PK type for the user
public interface IIdentityUserLogin : IEntity, IRememberBeingDirty
{
///
- /// The login provider for the login (i.e. Facebook, Google)
+ /// Gets or sets the login provider for the login (i.e. Facebook, Google)
///
string LoginProvider { get; set; }
///
- /// Key representing the login for the provider
+ /// Gets or sets key representing the login for the provider
///
string ProviderKey { get; set; }
///
- /// User Id for the user who owns this login
+ /// Gets or sets user Id for the user who owns this login
///
- int UserId { get; set; }
+ string UserId { get; set; } // TODO: This should be able to be used by both users and members
///
- /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider
+ /// Gets or sets any arbitrary data for the user and external provider - like user tokens returned from the provider
///
string UserData { get; set; }
}
diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs
deleted file mode 100644
index 093e42c1e7..0000000000
--- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs
+++ /dev/null
@@ -1,115 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Umbraco.Core.Models.Identity
-{
- ///
- /// Default IUser implementation
- ///
- ///
- ///
- /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want
- /// references to that so we will create our own here
- ///
- public class IdentityUser
- where TLogin : IIdentityUserLogin
- //NOTE: Making our role id a string
- where TRole : IdentityUserRole
- where TClaim : IdentityUserClaim
- {
- ///
- /// Initializes a new instance of the class.
- ///
- public IdentityUser()
- {
- Claims = new List();
- Roles = new List();
- Logins = new List();
- }
-
- ///
- /// Last login date
- ///
- public virtual DateTime? LastLoginDateUtc { get; set; }
-
- ///
- /// Email
- ///
- public virtual string Email { get; set; }
-
- ///
- /// True if the email is confirmed, default is false
- ///
- public virtual bool EmailConfirmed { get; set; }
-
- ///
- /// The salted/hashed form of the user password
- ///
- public virtual string PasswordHash { get; set; }
-
- ///
- /// A random value that should change whenever a users credentials have changed (password changed, login removed)
- ///
- public virtual string SecurityStamp { get; set; }
-
- ///
- /// PhoneNumber for the user
- ///
- public virtual string PhoneNumber { get; set; }
-
- ///
- /// True if the phone number is confirmed, default is false
- ///
- public virtual bool PhoneNumberConfirmed { get; set; }
-
- ///
- /// Is two factor enabled for the user
- ///
- public virtual bool TwoFactorEnabled { get; set; }
-
- ///
- /// DateTime in UTC when lockout ends, any time in the past is considered not locked out.
- ///
- public virtual DateTime? LockoutEndDateUtc { get; set; }
-
- ///
- /// DateTime in UTC when the password was last changed.
- ///
- public virtual DateTime? LastPasswordChangeDateUtc { get; set; }
-
- ///
- /// Is lockout enabled for this user
- ///
- public virtual bool LockoutEnabled { get; set; }
-
- ///
- /// Used to record failures for the purposes of lockout
- ///
- public virtual int AccessFailedCount { get; set; }
-
- ///
- /// Navigation property for user roles
- ///
- public virtual ICollection Roles { get; }
-
- ///
- /// Navigation property for user claims
- ///
- public virtual ICollection Claims { get; }
-
- ///
- /// Navigation property for user logins
- ///
- public virtual ICollection Logins { get; }
-
- ///
- /// User ID (Primary Key)
- ///
- public virtual TKey Id { get; set; }
-
- ///
- /// User name
- ///
- public virtual string UserName { get; set; }
- }
-}
diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs
deleted file mode 100644
index e117d2fd13..0000000000
--- a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-namespace Umbraco.Core.Models.Identity
-{
- ///
- /// EntityType that represents one specific user claim
- ///
- ///
- ///
- ///
- /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want
- /// references to that so we will create our own here
- ///
- public class IdentityUserClaim
- {
- ///
- /// Primary key
- ///
- ///
- public virtual int Id { get; set; }
-
- ///
- /// User Id for the user who owns this login
- ///
- ///
- public virtual TKey UserId { get; set; }
-
- ///
- /// Claim type
- ///
- ///
- public virtual string ClaimType { get; set; }
-
- ///
- /// Claim value
- ///
- ///
- public virtual string ClaimValue { get; set; }
- }
-}
diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs
index c13b28461d..5974822c20 100644
--- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs
+++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models.Identity
@@ -9,14 +9,20 @@ namespace Umbraco.Core.Models.Identity
///
public class IdentityUserLogin : EntityBase, IIdentityUserLogin
{
- public IdentityUserLogin(string loginProvider, string providerKey, int userId)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public IdentityUserLogin(string loginProvider, string providerKey, string userId)
{
LoginProvider = loginProvider;
ProviderKey = providerKey;
UserId = userId;
}
- public IdentityUserLogin(int id, string loginProvider, string providerKey, int userId, DateTime createDate)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public IdentityUserLogin(int id, string loginProvider, string providerKey, string userId, DateTime createDate)
{
Id = id;
LoginProvider = loginProvider;
@@ -32,7 +38,7 @@ namespace Umbraco.Core.Models.Identity
public string ProviderKey { get; set; }
///
- public int UserId { get; set; }
+ public string UserId { get; set; }
///
public string UserData { get; set; }
diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs
deleted file mode 100644
index ba9e87e46c..0000000000
--- a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-namespace Umbraco.Core.Models.Identity
-{
- ///
- /// EntityType that represents a user belonging to a role
- ///
- ///
- ///
- ///
- /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want
- /// references to that so we will create our own here
- ///
- public class IdentityUserRole
- {
- ///
- /// UserId for the user that is in the role
- ///
- ///
- public virtual TKey UserId { get; set; }
-
- ///
- /// RoleId for the role
- ///
- ///
- public virtual TKey RoleId { get; set; }
- }
-}
diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs
index a7771015a0..c72f4fac7c 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs
@@ -42,6 +42,7 @@ namespace Umbraco.Web.Models.Mapping
dest.Description = originalProp.PropertyType.Description;
dest.Label = originalProp.PropertyType.Name;
dest.HideLabel = valEditor.HideLabel;
+ dest.LabelOnTop = originalProp.PropertyType.LabelOnTop;
//add the validation information
dest.Validation.Mandatory = originalProp.PropertyType.Mandatory;
diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs
index e431614afc..456e23b68a 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs
@@ -27,6 +27,7 @@ namespace Umbraco.Web.Models.Mapping
dest.Description = property.PropertyType.Description;
dest.Label = property.PropertyType.Name;
dest.DataType = DataTypeService.GetDataType(property.PropertyType.DataTypeId);
+ dest.LabelOnTop = property.PropertyType.LabelOnTop;
}
}
}
diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs
index a74b0467e2..7740685615 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs
@@ -39,7 +39,7 @@ namespace Umbraco.Web.Models.Mapping
target.Id = source.Id;
target.IsActive = true;
target.Label = source.Name;
-}
+ }
private void Map(IProperty source, ContentPropertyBasic target, MapperContext context)
{
diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
index 34408469d4..0ed781fe10 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
@@ -262,6 +262,7 @@ namespace Umbraco.Web.Models.Mapping
target.Alias = source.Alias;
target.Description = source.Description;
target.SortOrder = source.SortOrder;
+ target.LabelOnTop = source.LabelOnTop;
}
// no MapAll - take care
@@ -373,6 +374,7 @@ namespace Umbraco.Web.Models.Mapping
target.Label = source.Label;
target.SortOrder = source.SortOrder;
target.Validation = source.Validation;
+ target.LabelOnTop = source.LabelOnTop;
}
// Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName
@@ -393,6 +395,7 @@ namespace Umbraco.Web.Models.Mapping
target.MemberCanViewProperty = source.MemberCanViewProperty;
target.SortOrder = source.SortOrder;
target.Validation = source.Validation;
+ target.LabelOnTop = source.LabelOnTop;
}
// Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations
diff --git a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs
index e2d4784aae..b41f5d4b19 100644
--- a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs
@@ -224,6 +224,7 @@ namespace Umbraco.Web.Models.Mapping
Id = p.Id,
Alias = p.Alias,
Description = p.Description,
+ LabelOnTop = p.LabelOnTop,
Editor = p.PropertyEditorAlias,
Validation = new PropertyTypeValidation
{
diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs
index 37097a16f6..4ded638e44 100644
--- a/src/Umbraco.Core/Models/PropertyType.cs
+++ b/src/Umbraco.Core/Models/PropertyType.cs
@@ -32,6 +32,7 @@ namespace Umbraco.Core.Models
private string _validationRegExp;
private string _validationRegExpMessage;
private ContentVariation _variations;
+ private bool _labelOnTop;
///
/// Initializes a new instance of the class.
@@ -192,6 +193,14 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage));
}
+ ///
+ [DataMember]
+ public bool LabelOnTop
+ {
+ get => _labelOnTop;
+ set => SetPropertyValueAndDetectChanges(value, ref _labelOnTop, nameof(LabelOnTop));
+ }
+
///
[DataMember]
public int SortOrder
@@ -274,7 +283,6 @@ namespace Umbraco.Core.Models
base.PerformDeepClone(clone);
var clonedEntity = (PropertyType) clone;
-
//need to manually assign the Lazy value as it will not be automatically mapped
if (PropertyGroupId != null)
{
diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs
index edc11bcac2..607c4748cc 100644
--- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs
+++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs
@@ -5,7 +5,6 @@ using System.Globalization;
using System.Security.Principal;
using System.Text;
using System.Threading;
-using Umbraco.Core.BackOffice;
namespace Umbraco.Core.Security
{
diff --git a/src/Umbraco.Core/BackOffice/BackOfficeUserPasswordCheckerResult.cs b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs
similarity index 87%
rename from src/Umbraco.Core/BackOffice/BackOfficeUserPasswordCheckerResult.cs
rename to src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs
index 7936fab682..c640c85d0c 100644
--- a/src/Umbraco.Core/BackOffice/BackOfficeUserPasswordCheckerResult.cs
+++ b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs
@@ -1,4 +1,4 @@
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
///
/// The result returned from the IBackOfficeUserPasswordChecker
diff --git a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs b/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs
similarity index 98%
rename from src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs
rename to src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs
index 7cbca0428a..395465cfb7 100644
--- a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs
+++ b/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs
@@ -1,10 +1,10 @@
-using System;
+using System;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using Umbraco.Core;
-using Umbraco.Core.BackOffice;
+using Umbraco.Core.Security;
namespace Umbraco.Extensions
{
diff --git a/src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs
similarity index 83%
rename from src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs
rename to src/Umbraco.Core/Security/IdentityAuditEventArgs.cs
index 1d51c45074..b9884c8e7d 100644
--- a/src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs
+++ b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs
@@ -1,7 +1,7 @@
-using System;
+using System;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
///
@@ -27,12 +27,12 @@ namespace Umbraco.Core.BackOffice
///
/// The user affected by the event raised
///
- public int AffectedUser { get; private set; }
+ public string AffectedUser { get; private set; }
///
/// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1
///
- public int PerformingUser { get; private set; }
+ public string PerformingUser { get; private set; }
///
/// An optional comment about the action being logged
@@ -53,19 +53,19 @@ namespace Umbraco.Core.BackOffice
///
///
///
- public IdentityAuditEventArgs(AuditEvent action, string ipAddress, int performingUser, string comment, int affectedUser, string affectedUsername)
+ public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUser, string affectedUsername)
{
DateTimeUtc = DateTime.UtcNow;
Action = action;
IpAddress = ipAddress;
- Comment = comment;
+ Comment = comment;
PerformingUser = performingUser;
AffectedUsername = affectedUsername;
AffectedUser = affectedUser;
}
- public IdentityAuditEventArgs(AuditEvent action, string ipAddress, int performingUser, string comment, string affectedUsername)
- : this(action, ipAddress, performingUser, comment, -1, affectedUsername)
+ public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUsername)
+ : this(action, ipAddress, performingUser, comment, Constants.Security.SuperUserIdAsString, affectedUsername)
{
}
diff --git a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
similarity index 83%
rename from src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs
rename to src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
index 9a60c5d64f..5fd9f23c92 100644
--- a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs
+++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
@@ -1,9 +1,9 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
///
@@ -15,13 +15,12 @@ namespace Umbraco.Core.BackOffice
// TODO: Ideally we remove this class and only deal with ClaimsIdentity as a best practice. All things relevant to our own
// identity are part of claims. This class would essentially become extension methods on a ClaimsIdentity for resolving
// values from it.
-
public static bool FromClaimsIdentity(ClaimsIdentity identity, out UmbracoBackOfficeIdentity backOfficeIdentity)
{
- //validate that all claims exist
+ // validate that all claims exist
foreach (var t in RequiredBackOfficeIdentityClaimTypes)
{
- //if the identity doesn't have the claim, or the claim value is null
+ // if the identity doesn't have the claim, or the claim value is null
if (identity.HasClaim(x => x.Type == t) == false || identity.HasClaim(x => x.Type == t && x.Value.IsNullOrWhiteSpace()))
{
backOfficeIdentity = null;
@@ -54,16 +53,21 @@ namespace Umbraco.Core.BackOffice
///
///
///
- public UmbracoBackOfficeIdentity(int userId, string username, string realName,
+ public UmbracoBackOfficeIdentity(string userId, string username, string realName,
IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
string securityStamp, IEnumerable allowedApps, IEnumerable roles)
: base(Enumerable.Empty(), Constants.Security.BackOfficeAuthenticationType) //this ctor is used to ensure the IsAuthenticated property is true
{
- if (allowedApps == null) throw new ArgumentNullException(nameof(allowedApps));
- if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
- if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
- if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
- if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
+ if (allowedApps == null)
+ throw new ArgumentNullException(nameof(allowedApps));
+ if (string.IsNullOrWhiteSpace(username))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
+ if (string.IsNullOrWhiteSpace(realName))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
+ if (string.IsNullOrWhiteSpace(culture))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
+ if (string.IsNullOrWhiteSpace(securityStamp))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, securityStamp, allowedApps, roles);
}
@@ -83,15 +87,19 @@ namespace Umbraco.Core.BackOffice
///
///
public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity,
- int userId, string username, string realName,
+ string userId, string username, string realName,
IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
string securityStamp, IEnumerable allowedApps, IEnumerable roles)
: base(childIdentity.Claims, Constants.Security.BackOfficeAuthenticationType)
{
- if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
- if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
- if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
- if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
+ if (string.IsNullOrWhiteSpace(username))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
+ if (string.IsNullOrWhiteSpace(realName))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
+ if (string.IsNullOrWhiteSpace(culture))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
+ if (string.IsNullOrWhiteSpace(securityStamp))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, securityStamp, allowedApps, roles);
}
@@ -118,13 +126,13 @@ namespace Umbraco.Core.BackOffice
///
/// Adds claims based on the ctor data
///
- private void AddRequiredClaims(int userId, string username, string realName,
+ private void AddRequiredClaims(string userId, string username, string realName,
IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
string securityStamp, IEnumerable allowedApps, IEnumerable roles)
{
//This is the id that 'identity' uses to check for the user id
if (HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false)
- AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, Issuer, Issuer, this));
if (HasClaim(x => x.Type == ClaimTypes.Name) == false)
AddClaim(new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, Issuer, Issuer, this));
@@ -205,7 +213,7 @@ namespace Umbraco.Core.BackOffice
public string SecurityStamp => this.FindFirstValue(Constants.Security.SecurityStampClaimType);
- public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray();
+ public string[] Roles => FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray();
///
/// Overridden to remove any temporary claims that shouldn't be copied
diff --git a/src/Umbraco.Core/Telemetry/TelemetryMarkerComponent.cs b/src/Umbraco.Core/Telemetry/TelemetryMarkerComponent.cs
new file mode 100644
index 0000000000..157cacd618
--- /dev/null
+++ b/src/Umbraco.Core/Telemetry/TelemetryMarkerComponent.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.Logging;
+using Umbraco.Core;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Hosting;
+using Umbraco.Core.IO;
+
+namespace Umbraco.Web.Telemetry
+{
+ public class TelemetryMarkerComponent : IComponent
+ {
+ private readonly ILogger _logger;
+ private readonly IRuntimeState _runtime;
+ private readonly IHostingEnvironment _hostingEnvironment;
+
+ public TelemetryMarkerComponent(ILogger logger, IRuntimeState runtime, IHostingEnvironment hostingEnvironment)
+ {
+ _logger = logger;
+ _runtime = runtime;
+ _hostingEnvironment = hostingEnvironment;
+ }
+
+ public void Initialize()
+ {
+ if (_runtime.Level != RuntimeLevel.Install && _runtime.Level != RuntimeLevel.Upgrade)
+ {
+ return;
+ }
+
+ var telemetricsFilePath = _hostingEnvironment.MapPathContentRoot(SystemFiles.TelemetricsIdentifier);
+
+ // Verify file does not exist already (if we are upgrading)
+ // In a clean install we know it would not exist
+ // If the site is upgraded and the file was removed it would re-create one
+ // NOTE: If user removed the marker file to opt out it would re-create a new guid marker file & potentially skew
+ if (_runtime.Level == RuntimeLevel.Upgrade && File.Exists(telemetricsFilePath))
+ {
+ _logger.LogWarning("When upgrading the anonymous telemetry file already existed on disk at {filePath}", telemetricsFilePath);
+ return;
+ }
+ if (_runtime.Level == RuntimeLevel.Install && File.Exists(telemetricsFilePath))
+ {
+ // No need to log for when level is install if file exists (As this component hit several times during install process)
+ return;
+ }
+
+ // We are a clean install or an upgrade without the marker file
+ // Generate GUID
+ var telemetrySiteIdentifier = Guid.NewGuid();
+
+ // Write file contents
+ try
+ {
+ File.WriteAllText(telemetricsFilePath, telemetrySiteIdentifier.ToString());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unable to create telemetry file at {filePath}", telemetricsFilePath);
+ }
+
+
+
+ }
+
+ public void Terminate()
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Telemetry/TelemetryMarkerComposer.cs b/src/Umbraco.Core/Telemetry/TelemetryMarkerComposer.cs
new file mode 100644
index 0000000000..ccb66c7b2a
--- /dev/null
+++ b/src/Umbraco.Core/Telemetry/TelemetryMarkerComposer.cs
@@ -0,0 +1,8 @@
+using Umbraco.Core;
+using Umbraco.Core.Composing;
+
+namespace Umbraco.Web.Telemetry
+{
+ public class TelemetryMarkerComposer : ComponentComposer, ICoreComposer
+ { }
+}
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
deleted file mode 100644
index b271f5aa41..0000000000
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
+++ /dev/null
@@ -1,919 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.Extensions.Options;
-using Umbraco.Core.Configuration;
-using Umbraco.Core.Configuration.Models;
-using Umbraco.Core.Mapping;
-using Umbraco.Core.Models;
-using Umbraco.Core.Models.Identity;
-using Umbraco.Core.Models.Membership;
-using Umbraco.Core.Scoping;
-using Umbraco.Core.Services;
-
-namespace Umbraco.Core.BackOffice
-{
- public class BackOfficeUserStore : DisposableObjectSlim,
- IUserPasswordStore,
- IUserEmailStore,
- IUserLoginStore,
- IUserRoleStore,
- IUserSecurityStampStore,
- IUserLockoutStore,
- IUserSessionStore
-
- // TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco
- //IUserTwoFactorStore,
- // TODO: This would require additional columns/tables for now people will need to implement this on their own
- //IUserPhoneNumberStore,
- // TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
- //IQueryableUserStore
- {
- private readonly IScopeProvider _scopeProvider;
- private readonly IUserService _userService;
- private readonly IEntityService _entityService;
- private readonly IExternalLoginService _externalLoginService;
- private readonly GlobalSettings _globalSettings;
- private readonly UmbracoMapper _mapper;
- private bool _disposed = false;
-
- public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper)
- {
- _scopeProvider = scopeProvider;
- _userService = userService;
- _entityService = entityService;
- _externalLoginService = externalLoginService;
- _globalSettings = globalSettings.Value;
- if (userService == null) throw new ArgumentNullException("userService");
- if (externalLoginService == null) throw new ArgumentNullException("externalLoginService");
- _mapper = mapper;
- _userService = userService;
- _externalLoginService = externalLoginService;
- }
-
- ///
- /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic.
- ///
- protected override void DisposeResources()
- {
- _disposed = true;
- }
-
- public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- return Task.FromResult(user.Id.ToString());
- }
-
- public Task GetUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- return Task.FromResult(user.UserName);
- }
-
- public Task SetUserNameAsync(BackOfficeIdentityUser user, string userName, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- user.UserName = userName;
- return Task.CompletedTask;
- }
-
- public Task GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
- {
- return GetUserNameAsync(user, cancellationToken);
- }
-
- public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken)
- {
- return SetUserNameAsync(user, normalizedName, cancellationToken);
- }
-
- ///
- /// Insert a new user
- ///
- ///
- ///
- ///
- public Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- //the password must be 'something' it could be empty if authenticating
- // with an external provider so we'll just generate one and prefix it, the
- // prefix will help us determine if the password hasn't actually been specified yet.
- //this will hash the guid with a salt so should be nicely random
- var aspHasher = new PasswordHasher();
- var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix +
- aspHasher.HashPassword(user, Guid.NewGuid().ToString("N"));
-
- var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue)
- {
- Language = user.Culture ?? _globalSettings.DefaultUILanguage,
- StartContentIds = user.StartContentIds ?? new int[] { },
- StartMediaIds = user.StartMediaIds ?? new int[] { },
- IsLockedOut = user.IsLockedOut,
- };
-
- // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
- var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
-
- UpdateMemberProperties(userEntity, user);
-
- _userService.Save(userEntity);
-
- if (!userEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details");
-
- //re-assign id
- user.Id = userEntity.Id;
-
- if (isLoginsPropertyDirty)
- {
- _externalLoginService.Save(
- user.Id,
- user.Logins.Select(x => new ExternalLogin(
- x.LoginProvider,
- x.ProviderKey,
- x.UserData)));
- }
-
- return Task.FromResult(IdentityResult.Success);
- }
-
- ///
- /// Update a user
- ///
- ///
- ///
- ///
- public Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- var asInt = user.Id.TryConvertTo();
- if (asInt == false)
- {
- throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
- }
-
- using (var scope = _scopeProvider.CreateScope())
- {
- var found = _userService.GetUserById(asInt.Result);
- if (found != null)
- {
- // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
- var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
-
- if (UpdateMemberProperties(found, user))
- {
- _userService.Save(found);
- }
-
- if (isLoginsPropertyDirty)
- {
- _externalLoginService.Save(
- found.Id,
- user.Logins.Select(x => new ExternalLogin(
- x.LoginProvider,
- x.ProviderKey,
- x.UserData)));
- }
- }
-
- scope.Complete();
- }
-
- return Task.FromResult(IdentityResult.Success);
- }
-
- ///
- /// Delete a user
- ///
- ///
- ///
- public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- var found = _userService.GetUserById(user.Id);
- if (found != null)
- {
- _userService.Delete(found);
- }
- _externalLoginService.DeleteUserLogins(user.Id);
-
- return Task.FromResult(IdentityResult.Success);
- }
-
- ///
- /// Finds a user
- ///
- ///
- ///
- ///
- public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
-
- var user = _userService.GetUserById(UserIdToInt(userId));
- if (user == null) return null;
-
- return await Task.FromResult(AssignLoginsCallback(_mapper.Map(user)));
- }
-
- ///
- /// Find a user by name
- ///
- ///
- ///
- ///
- public async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- var user = _userService.GetByUsername(userName);
- if (user == null)
- {
- return null;
- }
-
- var result = AssignLoginsCallback(_mapper.Map(user));
-
- return await Task.FromResult(result);
- }
-
- ///
- /// Set the user password hash
- ///
- ///
- ///
- ///
- public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash));
- if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash));
-
- user.PasswordHash = passwordHash;
- user.PasswordConfig = null; // Clear this so that it's reset at the repository level
-
- return Task.CompletedTask;
- }
-
- ///
- /// Get the user password hash
- ///
- ///
- ///
- ///
- public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- return Task.FromResult(user.PasswordHash);
- }
-
- ///
- /// Returns true if a user has a password set
- ///
- ///
- ///
- ///
- public Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false);
- }
-
- ///
- /// Set the user email
- ///
- ///
- ///
- ///
- public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(email));
-
- user.Email = email;
-
- return Task.CompletedTask;
- }
-
- ///
- /// Get the user email
- ///
- ///
- ///
- ///
- public Task GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- return Task.FromResult(user.Email);
- }
-
- ///
- /// Returns true if the user email is confirmed
- ///
- ///
- ///
- ///
- public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- return Task.FromResult(user.EmailConfirmed);
- }
-
- ///
- /// Sets whether the user email is confirmed
- ///
- ///
- ///
- ///
- public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- user.EmailConfirmed = confirmed;
- return Task.CompletedTask;
- }
-
- ///
- /// Returns the user associated with this email
- ///
- ///
- ///
- ///
- public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- var user = _userService.GetByEmail(email);
- var result = user == null
- ? null
- : _mapper.Map(user);
-
- return Task.FromResult(AssignLoginsCallback(result));
- }
-
- public Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
- {
- return GetEmailAsync(user, cancellationToken);
- }
-
- public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
- {
- return SetEmailAsync(user, normalizedEmail, cancellationToken);
- }
-
- ///
- /// Adds a user login with the specified provider and key
- ///
- ///
- ///
- ///
- ///
- public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- if (login == null) throw new ArgumentNullException(nameof(login));
-
- var logins = user.Logins;
- var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id);
- var userLogin = instance;
- logins.Add(userLogin);
-
- return Task.CompletedTask;
- }
-
- ///
- /// Removes the user login with the specified combination if it exists
- ///
- ///
- ///
- ///
- ///
- ///
- public Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- var userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
- if (userLogin != null) user.Logins.Remove(userLogin);
-
- return Task.CompletedTask;
- }
-
- ///
- /// Returns the linked accounts for this user
- ///
- ///
- ///
- ///
- public Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- return Task.FromResult((IList)
- user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList());
- }
-
- ///
- /// Returns the user associated with this login
- ///
- ///
- public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
-
- //get all logins associated with the login id
- var result = _externalLoginService.Find(loginProvider, providerKey).ToArray();
- if (result.Any())
- {
- //return the first user that matches the result
- BackOfficeIdentityUser output = null;
- foreach (var l in result)
- {
- var user = _userService.GetUserById(l.UserId);
- if (user != null)
- {
- output = _mapper.Map(user);
- break;
- }
- }
-
- return Task.FromResult(AssignLoginsCallback(output));
- }
-
- return Task.FromResult(null);
- }
-
-
- ///
- /// Adds a user to a role (user group)
- ///
- ///
- ///
- ///
- ///
- public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName));
- if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
-
- var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
-
- if (userRole == null)
- {
- user.AddRole(normalizedRoleName);
- }
-
- return Task.CompletedTask;
- }
-
- ///
- /// Removes the role (user group) for the user
- ///
- ///
- ///
- ///
- ///
- public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (user == null) throw new ArgumentNullException(nameof(user));
- if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName));
- if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
-
- var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
-
- if (userRole != null)
- {
- user.Roles.Remove(userRole);
- }
-
- return Task.CompletedTask;
- }
-
- ///
- /// Returns the roles (user groups) for this user
- ///
- ///
- ///
- ///
- public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList());
- }
-
- ///
- /// Returns true if a user is in the role
- ///
- ///
- ///
- ///
- ///
- public Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName));
- }
-
- ///
- /// Lists all users of a given role.
- ///
- ///
- /// Identity Role names are equal to Umbraco UserGroup alias.
- ///
- public Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName));
-
- var userGroup = _userService.GetUserGroupByAlias(normalizedRoleName);
-
- var users = _userService.GetAllInGroup(userGroup.Id);
- IList backOfficeIdentityUsers = users.Select(x => _mapper.Map(x)).ToList();
-
- return Task.FromResult(backOfficeIdentityUsers);
- }
-
- ///
- /// Set the security stamp for the user
- ///
- ///
- ///
- ///
- ///
- public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- user.SecurityStamp = stamp;
- return Task.CompletedTask;
- }
-
- ///
- /// Get the user security stamp
- ///
- ///
- ///
- ///
- public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- //the stamp cannot be null, so if it is currently null then we'll just return a hash of the password
- return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace()
- ? user.PasswordHash.GenerateHash()
- : user.SecurityStamp);
- }
-
- private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user)
- {
- if (user != null)
- {
- user.SetLoginsCallback(new Lazy>(() =>
- _externalLoginService.GetAll(user.Id)));
- }
- return user;
- }
-
- #region IUserLockoutStore
-
- ///
- /// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered not locked out.
- ///
- ///
- ///
- ///
- ///
- /// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status
- ///
- public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- return user.LockoutEndDateUtc.HasValue
- ? Task.FromResult(DateTimeOffset.MaxValue)
- : Task.FromResult(DateTimeOffset.MinValue);
- }
-
- ///
- /// Locks a user out until the specified end date (set to a past date, to unlock a user)
- ///
- ///
- ///
- ///
- ///
- /// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status
- ///
- public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- user.LockoutEndDateUtc = lockoutEnd.Value.UtcDateTime;
- return Task.CompletedTask;
- }
-
- ///
- /// Used to record when an attempt to access the user has failed
- ///
- ///
- ///
- ///
- public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- user.AccessFailedCount++;
- return Task.FromResult(user.AccessFailedCount);
- }
-
- ///
- /// Used to reset the access failed count, typically after the account is successfully accessed
- ///
- ///
- ///
- ///
- public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- user.AccessFailedCount = 0;
- return Task.CompletedTask;
- }
-
- ///
- /// Returns the current number of failed access attempts. This number usually will be reset whenever the password is
- /// verified or the account is locked out.
- ///
- ///
- ///
- ///
- public Task GetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- return Task.FromResult(user.AccessFailedCount);
- }
-
- ///
- /// Returns true
- ///
- ///
- ///
- ///
- public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
- return Task.FromResult(user.LockoutEnabled);
- }
-
- ///
- /// Doesn't actually perform any function, users can always be locked out
- ///
- ///
- ///
- ///
- public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (user == null) throw new ArgumentNullException(nameof(user));
-
- user.LockoutEnabled = enabled;
- return Task.CompletedTask;
- }
- #endregion
-
- private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser)
- {
- var anythingChanged = false;
-
- //don't assign anything if nothing has changed as this will trigger the track changes of the model
-
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastLoginDateUtc))
- || (user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false)
- || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value)
- {
- anythingChanged = true;
- //if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
- var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime();
- user.LastLoginDate = dt;
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc))
- || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false)
- || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value)
- {
- anythingChanged = true;
- user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime();
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.EmailConfirmed))
- || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false)
- || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed))
- {
- anythingChanged = true;
- user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null;
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name))
- && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false)
- {
- anythingChanged = true;
- user.Name = identityUser.Name;
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Email))
- && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false)
- {
- anythingChanged = true;
- user.Email = identityUser.Email;
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.AccessFailedCount))
- && user.FailedPasswordAttempts != identityUser.AccessFailedCount)
- {
- anythingChanged = true;
- user.FailedPasswordAttempts = identityUser.AccessFailedCount;
- }
- if (user.IsLockedOut != identityUser.IsLockedOut)
- {
- anythingChanged = true;
- user.IsLockedOut = identityUser.IsLockedOut;
-
- if (user.IsLockedOut)
- {
- //need to set the last lockout date
- user.LastLockoutDate = DateTime.Now;
- }
-
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName))
- && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false)
- {
- anythingChanged = true;
- user.Username = identityUser.UserName;
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash))
- && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false)
- {
- anythingChanged = true;
- user.RawPasswordValue = identityUser.PasswordHash;
- user.PasswordConfiguration = identityUser.PasswordConfig;
- }
-
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Culture))
- && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false)
- {
- anythingChanged = true;
- user.Language = identityUser.Culture;
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartMediaIds))
- && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false)
- {
- anythingChanged = true;
- user.StartMediaIds = identityUser.StartMediaIds;
- }
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartContentIds))
- && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false)
- {
- anythingChanged = true;
- user.StartContentIds = identityUser.StartContentIds;
- }
- if (user.SecurityStamp != identityUser.SecurityStamp)
- {
- anythingChanged = true;
- user.SecurityStamp = identityUser.SecurityStamp;
- }
-
- // TODO: Fix this for Groups too
- if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups)))
- {
- var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray();
-
- var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray();
- var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray();
-
- var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray();
-
- if (userGroupAliases.ContainsAll(combinedAliases) == false
- || combinedAliases.ContainsAll(userGroupAliases) == false)
- {
- anythingChanged = true;
-
- //clear out the current groups (need to ToArray since we are modifying the iterator)
- user.ClearGroups();
-
- //go lookup all these groups
- var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray();
-
- //use all of the ones assigned and add them
- foreach (var group in groups)
- {
- user.AddGroup(group);
- }
-
- //re-assign
- identityUser.Groups = groups;
- }
- }
-
- //we should re-set the calculated start nodes
- identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService);
- identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService);
-
- //reset all changes
- identityUser.ResetDirtyProperties(false);
-
- return anythingChanged;
- }
-
- private void ThrowIfDisposed()
- {
- if (_disposed) throw new ObjectDisposedException(GetType().Name);
- }
-
- public Task ValidateSessionIdAsync(string userId, string sessionId)
- {
- Guid guidSessionId;
- if (Guid.TryParse(sessionId, out guidSessionId))
- {
- return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId));
- }
-
- return Task.FromResult(false);
- }
-
- private static int UserIdToInt(string userId)
- {
- var attempt = userId.TryConvertTo();
- if (attempt.Success) return attempt.Result;
-
- throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception);
- }
- }
-}
diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs
index 09a8523cb9..c700938534 100644
--- a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs
+++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs
@@ -1,7 +1,7 @@
-using Microsoft.Extensions.DependencyInjection;
-using Umbraco.Core.BackOffice;
+using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core.DependencyInjection;
using Umbraco.Core.Mapping;
+using Umbraco.Core.Security;
using Umbraco.Web.Models.Mapping;
namespace Umbraco.Core.Composing.CompositionExtensions
@@ -19,7 +19,6 @@ namespace Umbraco.Core.Composing.CompositionExtensions
builder.Services.AddUnique();
builder.WithCollectionBuilder()
- .Add()
.Add()
.Add()
.Add()
diff --git a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs
index 1e775abaa9..cd89ebc046 100644
--- a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs
@@ -1,4 +1,8 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -13,6 +17,7 @@ using Umbraco.Core.Scoping;
using Umbraco.Core.Sync;
using Umbraco.Infrastructure.HealthCheck;
using Umbraco.Web.HealthCheck;
+using Umbraco.Web.HealthCheck.NotificationMethods;
namespace Umbraco.Infrastructure.HostedServices
{
@@ -31,6 +36,19 @@ namespace Umbraco.Infrastructure.HostedServices
private readonly ILogger _logger;
private readonly IProfilingLogger _profilingLogger;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration for health check settings.
+ /// The collection of healthchecks.
+ /// The collection of healthcheck notification methods.
+ /// Representation of the state of the Umbraco runtime.
+ /// Provider of server registrations to the distributed cache.
+ /// Representation of the main application domain.
+ /// Provides scopes for database operations.
+ /// The typed logger.
+ /// The profiling logger.
+ /// Parser of crontab expressions.
public HealthCheckNotifier(
IOptions healthChecksSettings,
HealthCheckCollection healthChecks,
@@ -42,8 +60,9 @@ namespace Umbraco.Infrastructure.HostedServices
ILogger logger,
IProfilingLogger profilingLogger,
ICronTabParser cronTabParser)
- : base(healthChecksSettings.Value.Notification.Period,
- healthChecksSettings.Value.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay))
+ : base(
+ healthChecksSettings.Value.Notification.Period,
+ healthChecksSettings.Value.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay))
{
_healthChecksSettings = healthChecksSettings.Value;
_healthChecks = healthChecks;
@@ -88,25 +107,25 @@ namespace Umbraco.Infrastructure.HostedServices
// Ensure we use an explicit scope since we are running on a background thread and plugin health
// checks can be making service/database calls so we want to ensure the CallContext/Ambient scope
// isn't used since that can be problematic.
- using (var scope = _scopeProvider.CreateScope())
+ using (IScope scope = _scopeProvider.CreateScope())
using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete"))
{
// Don't notify for any checks that are disabled, nor for any disabled just for notifications.
- var disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks
+ Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks
.Select(x => x.Id)
.Union(_healthChecksSettings.DisabledChecks
.Select(x => x.Id))
.Distinct()
.ToArray();
- var checks = _healthChecks
+ IEnumerable checks = _healthChecks
.Where(x => disabledCheckIds.Contains(x.Id) == false);
var results = new HealthCheckResults(checks);
results.LogResults();
// Send using registered notification methods that are enabled.
- foreach (var notificationMethod in _notifications.Where(x => x.Enabled))
+ foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled))
{
await notificationMethod.SendAsync(results);
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
index 39ac0f3d87..6a56b6f98e 100644
--- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -24,7 +27,24 @@ namespace Umbraco.Infrastructure.HostedServices
private readonly IServerRegistrar _serverRegistrar;
private readonly IHttpClientFactory _httpClientFactory;
- public KeepAlive(IRequestAccessor requestAccessor, IMainDom mainDom, IOptions keepAliveSettings, ILogger logger, IProfilingLogger profilingLogger, IServerRegistrar serverRegistrar, IHttpClientFactory httpClientFactory)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Accessor for the current request.
+ /// Representation of the main application domain.
+ /// The configuration for keep alive settings.
+ /// The typed logger.
+ /// The profiling logger.
+ /// Provider of server registrations to the distributed cache.
+ /// Factory for instances.
+ public KeepAlive(
+ IRequestAccessor requestAccessor,
+ IMainDom mainDom,
+ IOptions keepAliveSettings,
+ ILogger logger,
+ IProfilingLogger profilingLogger,
+ IServerRegistrar serverRegistrar,
+ IHttpClientFactory httpClientFactory)
: base(TimeSpan.FromMinutes(5), DefaultDelay)
{
_requestAccessor = requestAccessor;
@@ -79,8 +99,8 @@ namespace Umbraco.Infrastructure.HostedServices
}
var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl);
- var httpClient = _httpClientFactory.CreateClient();
- await httpClient.SendAsync(request);
+ HttpClient httpClient = _httpClientFactory.CreateClient();
+ _ = await httpClient.SendAsync(request);
}
catch (Exception ex)
{
diff --git a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs
index 1cf2da05f9..ca87d3e84e 100644
--- a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs
@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -27,7 +30,24 @@ namespace Umbraco.Infrastructure.HostedServices
private readonly ILogger _logger;
private readonly IScopeProvider _scopeProvider;
- public LogScrubber(IMainDom mainDom, IServerRegistrar serverRegistrar, IAuditService auditService, IOptions settings, IScopeProvider scopeProvider, ILogger logger, IProfilingLogger profilingLogger)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Representation of the main application domain.
+ /// Provider of server registrations to the distributed cache.
+ /// Service for handling audit operations.
+ /// The configuration for logging settings.
+ /// Provides scopes for database operations.
+ /// The typed logger.
+ /// The profiling logger.
+ public LogScrubber(
+ IMainDom mainDom,
+ IServerRegistrar serverRegistrar,
+ IAuditService auditService,
+ IOptions settings,
+ IScopeProvider scopeProvider,
+ ILogger logger,
+ IProfilingLogger profilingLogger)
: base(TimeSpan.FromHours(4), DefaultDelay)
{
_mainDom = mainDom;
@@ -39,32 +59,34 @@ namespace Umbraco.Infrastructure.HostedServices
_profilingLogger = profilingLogger;
}
- internal override async Task PerformExecuteAsync(object state)
+ internal override Task PerformExecuteAsync(object state)
{
switch (_serverRegistrar.GetCurrentServerRole())
{
case ServerRole.Replica:
_logger.LogDebug("Does not run on replica servers.");
- return;
+ return Task.CompletedTask;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
- return;
+ return Task.CompletedTask;
}
// Ensure we do not run if not main domain, but do NOT lock it
if (_mainDom.IsMainDom == false)
{
_logger.LogDebug("Does not run if not MainDom.");
- return;
+ return Task.CompletedTask;
}
// Ensure we use an explicit scope since we are running on a background thread.
- using (var scope = _scopeProvider.CreateScope())
+ using (IScope scope = _scopeProvider.CreateScope())
using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete"))
{
_auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes);
- scope.Complete();
+ _ = scope.Complete();
}
+
+ return Task.CompletedTask;
}
}
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
index ee77326115..7e9354523a 100644
--- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
@@ -13,44 +16,54 @@ namespace Umbraco.Infrastructure.HostedServices
///
public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
{
+ ///
+ /// The default delay to use for recurring tasks for the first run after application start-up if no alternative is configured.
+ ///
protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3);
private readonly TimeSpan _period;
private readonly TimeSpan _delay;
private Timer _timer;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Timepsan representing how often the task should recur.
+ /// Timespan represeting the initial delay after application start-up before the first run of the task occurs.
protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay)
{
_period = period;
_delay = delay;
}
+ ///
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds);
return Task.CompletedTask;
}
- public async void ExecuteAsync(object state)
- {
+ ///
+ /// Executes the task.
+ ///
+ /// The task state.
+ public async void ExecuteAsync(object state) =>
// Delegate work to method returning a task, that can be called and asserted in a unit test.
// Without this there can be behaviour where tests pass, but an error within them causes the test
// running process to crash.
// Hat-tip: https://stackoverflow.com/a/14207615/489433
await PerformExecuteAsync(state);
- }
internal abstract Task PerformExecuteAsync(object state);
+ ///
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
- public void Dispose()
- {
- _timer?.Dispose();
- }
+ ///
+ public void Dispose() => _timer?.Dispose();
}
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
new file mode 100644
index 0000000000..95b09d5498
--- /dev/null
+++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
@@ -0,0 +1,132 @@
+using Newtonsoft.Json;
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Umbraco.Core;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Hosting;
+using Umbraco.Core.IO;
+using Umbraco.Infrastructure.HostedServices;
+
+namespace Umbraco.Web.Telemetry
+{
+ public class ReportSiteTask : RecurringHostedServiceBase
+ {
+ private readonly ILogger _logger;
+ private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly IUmbracoVersion _umbracoVersion;
+ private static HttpClient s_httpClient;
+
+ public ReportSiteTask(
+ ILogger logger,
+ IHostingEnvironment hostingEnvironment,
+ IUmbracoVersion umbracoVersion)
+ : base(TimeSpan.FromDays(1), TimeSpan.FromMinutes(1))
+ {
+ _logger = logger;
+ _hostingEnvironment = hostingEnvironment;
+ _umbracoVersion = umbracoVersion;
+ s_httpClient = new HttpClient();
+ }
+
+ ///
+ /// Runs the background task to send the anynomous ID
+ /// to telemetry service
+ ///
+ internal override async Task PerformExecuteAsync(object state)
+ {
+ // Try & find file at '/umbraco/telemetrics-id.umb'
+ var telemetricsFilePath = _hostingEnvironment.MapPathContentRoot(SystemFiles.TelemetricsIdentifier);
+
+ if (File.Exists(telemetricsFilePath) == false)
+ {
+ // Some users may have decided to not be tracked by deleting/removing the marker file
+ _logger.LogWarning("No telemetry marker file found at '{filePath}' and will not report site to telemetry service", telemetricsFilePath);
+
+ return;
+ }
+
+
+ string telemetricsFileContents;
+ try
+ {
+ // Open file & read its contents
+ // It may throw due to file permissions or file locking
+ telemetricsFileContents = File.ReadAllText(telemetricsFilePath);
+ }
+ catch (Exception ex)
+ {
+ // Silently swallow ex - but lets log it (ReadAllText throws a ton of different types of ex)
+ // Hence the use of general exception type
+ _logger.LogError(ex, "Error in reading file contents of telemetry marker file found at '{filePath}'", telemetricsFilePath);
+
+ // Exit out early, but mark this task to be repeated in case its a file lock so it can be rechecked the next time round
+ return;
+ }
+
+
+ // Parse as a GUID & verify its a GUID and not some random string
+ // In case of users may have messed or decided to empty the file contents or put in something random
+ if (Guid.TryParse(telemetricsFileContents, out var telemetrySiteIdentifier) == false)
+ {
+ // Some users may have decided to mess with file contents
+ _logger.LogWarning("The telemetry marker file found at '{filePath}' with '{telemetrySiteId}' is not a valid identifier for the telemetry service", telemetricsFilePath, telemetrySiteIdentifier);
+
+ return;
+ }
+
+ try
+ {
+
+ // Send data to LIVE telemetry
+ s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
+
+#if DEBUG
+ // Send data to DEBUG telemetry service
+ s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
+#endif
+
+ s_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
+
+ using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/"))
+ {
+ var postData = new TelemetryReportData { Id = telemetrySiteIdentifier, Version = _umbracoVersion.SemanticVersion.ToSemanticString() };
+ request.Content = new StringContent(JsonConvert.SerializeObject(postData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header
+
+ // Set a low timeout - no need to use a larger default timeout for this POST request
+ s_httpClient.Timeout = new TimeSpan(0, 0, 1);
+
+ // Make a HTTP Post to telemetry service
+ // https://telemetry.umbraco.com/installs/
+ // Fire & Forget, do not need to know if its a 200, 500 etc
+ using (HttpResponseMessage response = await s_httpClient.SendAsync(request))
+ {
+ }
+ }
+ }
+ catch
+ {
+ // Silently swallow
+ // The user does not need the logs being polluted if our service has fallen over or is down etc
+ // Hence only loggigng this at a more verbose level (Which users should not be using in prod)
+ _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service");
+ }
+ }
+
+ [DataContract]
+ private class TelemetryReportData
+ {
+ [DataMember(Name = "id")]
+ public Guid Id { get; set; }
+
+ [DataMember(Name = "version")]
+ public string Version { get; set; }
+ }
+
+
+ }
+}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
index f48e64ad2d..4c235255c2 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
@@ -1,8 +1,13 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Umbraco.Core;
+using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Core.Sync;
using Umbraco.Web;
@@ -19,18 +24,35 @@ namespace Umbraco.Infrastructure.HostedServices
private readonly IContentService _contentService;
private readonly ILogger _logger;
private readonly IMainDom _mainDom;
- private readonly IRuntimeState _runtime;
+ private readonly IRuntimeState _runtimeState;
private readonly IServerMessenger _serverMessenger;
private readonly IBackOfficeSecurityFactory _backofficeSecurityFactory;
private readonly IServerRegistrar _serverRegistrar;
private readonly IUmbracoContextFactory _umbracoContextFactory;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Representation of the state of the Umbraco runtime.
+ /// Representation of the main application domain.
+ /// Provider of server registrations to the distributed cache.
+ /// Service for handling content operations.
+ /// Service for creating and managing Umbraco context.
+ /// The typed logger.
+ /// Service broadcasting cache notifications to registered servers.
+ /// Creates and manages instances.
public ScheduledPublishing(
- IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService,
- IUmbracoContextFactory umbracoContextFactory, ILogger logger, IServerMessenger serverMessenger, IBackOfficeSecurityFactory backofficeSecurityFactory)
+ IRuntimeState runtimeState,
+ IMainDom mainDom,
+ IServerRegistrar serverRegistrar,
+ IContentService contentService,
+ IUmbracoContextFactory umbracoContextFactory,
+ ILogger logger,
+ IServerMessenger serverMessenger,
+ IBackOfficeSecurityFactory backofficeSecurityFactory)
: base(TimeSpan.FromMinutes(1), DefaultDelay)
{
- _runtime = runtime;
+ _runtimeState = runtimeState;
_mainDom = mainDom;
_serverRegistrar = serverRegistrar;
_contentService = contentService;
@@ -40,35 +62,35 @@ namespace Umbraco.Infrastructure.HostedServices
_backofficeSecurityFactory = backofficeSecurityFactory;
}
- internal override async Task PerformExecuteAsync(object state)
+ internal override Task PerformExecuteAsync(object state)
{
if (Suspendable.ScheduledPublishing.CanRun == false)
{
- return;
+ return Task.CompletedTask;
}
switch (_serverRegistrar.GetCurrentServerRole())
{
case ServerRole.Replica:
_logger.LogDebug("Does not run on replica servers.");
- return;
+ return Task.CompletedTask;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
- return;
+ return Task.CompletedTask;
}
// Ensure we do not run if not main domain, but do NOT lock it
if (_mainDom.IsMainDom == false)
{
_logger.LogDebug("Does not run if not MainDom.");
- return;
+ return Task.CompletedTask;
}
// Do NOT run publishing if not properly running
- if (_runtime.Level != RuntimeLevel.Run)
+ if (_runtimeState.Level != RuntimeLevel.Run)
{
_logger.LogDebug("Does not run if run level is not Run.");
- return;
+ return Task.CompletedTask;
}
try
@@ -85,16 +107,17 @@ namespace Umbraco.Infrastructure.HostedServices
// - and we should definitively *not* have to flush it here (should be auto)
//
_backofficeSecurityFactory.EnsureBackOfficeSecurity();
- using var contextReference = _umbracoContextFactory.EnsureUmbracoContext();
+ using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
try
{
// Run
- var result = _contentService.PerformScheduledPublish(DateTime.Now);
- foreach (var grouped in result.GroupBy(x => x.Result))
+ IEnumerable result = _contentService.PerformScheduledPublish(DateTime.Now);
+ foreach (IGrouping grouped in result.GroupBy(x => x.Result))
{
_logger.LogInformation(
"Scheduled publishing result: '{StatusCount}' items with status {Status}",
- grouped.Count(), grouped.Key);
+ grouped.Count(),
+ grouped.Key);
}
}
finally
@@ -112,7 +135,7 @@ namespace Umbraco.Infrastructure.HostedServices
_logger.LogError(ex, "Failed.");
}
- return;
+ return Task.CompletedTask;
}
}
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
index 8d669178b0..3c291f187b 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -20,6 +23,10 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration
///
/// Initializes a new instance of the class.
///
+ /// Representation of the state of the Umbraco runtime.
+ /// Service broadcasting cache notifications to registered servers.
+ /// The typed logger.
+ /// The configuration for global settings.
public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings)
: base(globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1))
{
@@ -28,11 +35,11 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration
_logger = logger;
}
- internal override async Task PerformExecuteAsync(object state)
+ internal override Task PerformExecuteAsync(object state)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
- return;
+ return Task.CompletedTask;
}
try
@@ -43,6 +50,8 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration
{
_logger.LogError(e, "Failed (will repeat).");
}
+
+ return Task.CompletedTask;
}
}
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
index 0384a071c6..25e975582d 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -18,11 +21,16 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration
private readonly IServerRegistrationService _serverRegistrationService;
private readonly IRequestAccessor _requestAccessor;
private readonly ILogger _logger;
- private GlobalSettings _globalSettings;
+ private readonly GlobalSettings _globalSettings;
///
/// Initializes a new instance of the class.
///
+ /// Representation of the state of the Umbraco runtime.
+ /// Services for server registrations.
+ /// Accessor for the current request.
+ /// The typed logger.
+ /// The configuration for global settings.
public TouchServerTask(IRuntimeState runtimeState, IServerRegistrationService serverRegistrationService, IRequestAccessor requestAccessor, ILogger logger, IOptions globalSettings)
: base(globalSettings.Value.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15))
{
@@ -33,18 +41,18 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration
_globalSettings = globalSettings.Value;
}
- internal override async Task PerformExecuteAsync(object state)
+ internal override Task PerformExecuteAsync(object state)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
- return;
+ return Task.CompletedTask;
}
var serverAddress = _requestAccessor.GetApplicationUrl()?.ToString();
if (serverAddress.IsNullOrWhiteSpace())
{
_logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
- return;
+ return Task.CompletedTask;
}
try
@@ -57,6 +65,8 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration
{
_logger.LogError(ex, "Failed to update server record in database.");
}
+
+ return Task.CompletedTask;
}
}
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
index e27b83c8f6..7e3f70d510 100644
--- a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -23,6 +26,12 @@ namespace Umbraco.Infrastructure.HostedServices
private readonly DirectoryInfo[] _tempFolders;
private readonly TimeSpan _age = TimeSpan.FromDays(1);
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Helper service for IO operations.
+ /// Representation of the main application domain.
+ /// The typed logger.
public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger)
: base(TimeSpan.FromMinutes(60), DefaultDelay)
{
@@ -33,33 +42,33 @@ namespace Umbraco.Infrastructure.HostedServices
_tempFolders = _ioHelper.GetTempFolders();
}
- internal override async Task PerformExecuteAsync(object state)
+ internal override Task PerformExecuteAsync(object state)
{
// Ensure we do not run if not main domain
if (_mainDom.IsMainDom == false)
{
_logger.LogDebug("Does not run if not MainDom.");
- return;
+ return Task.CompletedTask;
}
- foreach (var folder in _tempFolders)
+ foreach (DirectoryInfo folder in _tempFolders)
{
CleanupFolder(folder);
}
- return;
+ return Task.CompletedTask;
}
private void CleanupFolder(DirectoryInfo folder)
{
- var result = _ioHelper.CleanFolder(folder, _age);
+ CleanFolderResult result = _ioHelper.CleanFolder(folder, _age);
switch (result.Status)
{
case CleanFolderResultStatus.FailedAsDoesNotExist:
_logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName);
break;
case CleanFolderResultStatus.FailedWithException:
- foreach (var error in result.Errors)
+ foreach (CleanFolderResult.Error error in result.Errors)
{
_logger.LogError(error.Exception, "Could not delete temp file {FileName}", error.ErroringFile.FullName);
}
@@ -74,8 +83,8 @@ namespace Umbraco.Infrastructure.HostedServices
return;
}
- var files = folder.GetFiles("*.*", SearchOption.AllDirectories);
- foreach (var file in files)
+ FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories);
+ foreach (FileInfo file in files)
{
if (DateTime.UtcNow - file.LastWriteTimeUtc > _age)
{
diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs
index 3a91fc3369..2616e3a926 100644
--- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs
+++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs
@@ -10,7 +10,7 @@ namespace Umbraco.Web.Migrations.PostMigrations
{
private readonly ICookieManager _cookieManager;
- public ClearCsrfCookies(ICookieManager cookieManager)
+ public ClearCsrfCookies(IMigrationContext context, ICookieManager cookieManager)
{
_cookieManager = cookieManager;
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index 3fee15c10a..c0a4f5bd35 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -9,6 +9,7 @@ using Umbraco.Core.Migrations.Upgrade.V_8_0_1;
using Umbraco.Core.Migrations.Upgrade.V_8_1_0;
using Umbraco.Core.Migrations.Upgrade.V_8_6_0;
using Umbraco.Core.Migrations.Upgrade.V_8_9_0;
+using Umbraco.Core.Migrations.Upgrade.V_8_10_0;
namespace Umbraco.Core.Migrations.Upgrade
{
@@ -194,7 +195,8 @@ namespace Umbraco.Core.Migrations.Upgrade
// to 8.9.0
To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}");
-
+// to 8.10.0
+ To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}");
//FINAL
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs
new file mode 100644
index 0000000000..206ea2be02
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs
@@ -0,0 +1,20 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_10_0
+{
+
+ public class AddPropertyTypeLabelOnTopColumn : MigrationBase
+ {
+ public AddPropertyTypeLabelOnTopColumn(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
+
+ AddColumnIfNotExists(columns, "labelOnTop");
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs
index f8bf6283f5..0b9572cb20 100644
--- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs
+++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs
@@ -814,7 +814,10 @@ namespace Umbraco.Core.Packaging
SortOrder = sortOrder,
Variations = property.Element("Variations") != null
? (ContentVariation)Enum.Parse(typeof(ContentVariation), property.Element("Variations").Value)
- : ContentVariation.Nothing
+ : ContentVariation.Nothing,
+ LabelOnTop = property.Element("LabelOnTop") != null
+ ? property.Element("LabelOnTop").Value.ToLowerInvariant().Equals("true")
+ : false
};
if (property.Element("Key") != null)
propertyType.Key = new Guid(property.Element("Key").Value);
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs
index 3e8d6e7496..572201c94a 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs
@@ -62,6 +62,10 @@ namespace Umbraco.Core.Persistence.Dtos
[Length(2000)]
public string Description { get; set; }
+ [Column("labelOnTop")]
+ [Constraint(Default = "0")]
+ public bool LabelOnTop { get; set; }
+
[Column("variations")]
[Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)]
public byte Variations { get; set; }
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
index 4c352a0134..d2001c00d5 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
@@ -44,6 +44,9 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("Description")]
public string Description { get; set; }
+ [Column("labelOnTop")]
+ public bool LabelOnTop { get; set; }
+
/* cmsMemberType */
[Column("memberCanEdit")]
public bool CanEdit { get; set; }
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs
index 028b760ba5..46bec34a49 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using NPoco;
using Umbraco.Core.Persistence.DatabaseAnnotations;
@@ -19,6 +19,8 @@ namespace Umbraco.Core.Persistence.Dtos
UserStartNodeDtos = new HashSet();
}
+ // TODO: We need to add a GUID for users and track external logins with that instead of the INT
+
[Column("id")]
[PrimaryKeyColumn(Name = "PK_user")]
public int Id { get; set; }
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs
index 74d2fe7ff0..aa4b20aa40 100644
--- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Persistence.Dtos;
@@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Factories
{
public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto)
{
- var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate)
+ var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId.ToString(), dto.CreateDate)
{
UserData = dto.UserData
};
@@ -26,7 +26,7 @@ namespace Umbraco.Core.Persistence.Factories
CreateDate = entity.CreateDate,
LoginProvider = entity.LoginProvider,
ProviderKey = entity.ProviderKey,
- UserId = entity.UserId,
+ UserId = int.Parse(entity.UserId), // TODO: This is temp until we change the ext logins to use GUIDs
UserData = entity.UserData
};
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs
index ab1de9cb40..29243d3a23 100644
--- a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs
@@ -132,7 +132,8 @@ namespace Umbraco.Core.Persistence.Factories
ValidationRegExp = propertyType.ValidationRegExp,
ValidationRegExpMessage = propertyType.ValidationRegExpMessage,
UniqueId = propertyType.Key,
- Variations = (byte)propertyType.Variations
+ Variations = (byte)propertyType.Variations,
+ LabelOnTop = propertyType.LabelOnTop
};
if (tabId != default)
diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs
index 8aa95fd13a..6892a26d91 100644
--- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs
@@ -30,6 +30,7 @@ namespace Umbraco.Core.Persistence.Mappers
DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder));
DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp));
DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage));
+ DefineMap(nameof(PropertyType.LabelOnTop), nameof(PropertyTypeDto.LabelOnTop));
DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias));
DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType));
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
index 58e05fa272..657159acc5 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
@@ -307,7 +307,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
SortOrder = dto.SortOrder,
ValidationRegExp = dto.ValidationRegExp,
ValidationRegExpMessage = dto.ValidationRegExpMessage,
- Variations = (ContentVariation)dto.Variations
+ Variations = (ContentVariation)dto.Variations,
+ LabelOnTop = dto.LabelOnTop
};
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs
index 33fd3af7fc..c3ed111ffb 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
@@ -13,6 +13,8 @@ using Umbraco.Core.Scoping;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
+ // TODO: We should update this to support both users and members. It means we would remove referential integrity from users
+ // and the user/member key would be a GUID (we also need to add a GUID to users)
internal class ExternalLoginRepository : NPocoRepositoryBase, IExternalLoginRepository
{
public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
index b199a7e8dc..c9a67a8543 100644
--- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
+++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
@@ -377,6 +377,7 @@ namespace Umbraco.Core.Runtime
builder.Services.AddUnique(factory => new LegacyPasswordSecurity());
builder.Services.AddUnique();
builder.Services.AddUnique();
+
builder.Services.AddUnique();
builder.Services.AddUnique();
}
diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs
index 87986a6318..65e8e343f7 100644
--- a/src/Umbraco.Infrastructure/Scoping/Scope.cs
+++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs
@@ -496,7 +496,7 @@ namespace Umbraco.Core.Scoping
// caching config
// true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true"
private bool LogUncompletedScopes => (_logUncompletedScopes
- ?? (_logUncompletedScopes = _coreDebugSettings.LogUncompletedScopes)).Value;
+ ?? (_logUncompletedScopes = _coreDebugSettings.LogIncompletedScopes)).Value;
///
public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds);
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs
similarity index 53%
rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs
rename to src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs
index 568c028e67..77f707d812 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs
@@ -1,33 +1,50 @@
-using System;
+using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
+using Umbraco.Core.Security;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
- public class BackOfficeClaimsPrincipalFactory : UserClaimsPrincipalFactory
- where TUser : BackOfficeIdentityUser
+ ///
+ /// A
+ ///
+ public class BackOfficeClaimsPrincipalFactory : UserClaimsPrincipalFactory
{
- public BackOfficeClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor)
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The user manager
+ /// The
+ public BackOfficeClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor)
: base(userManager, optionsAccessor)
{
}
- public override async Task CreateAsync(TUser user)
+ ///
+ ///
+ /// Returns a custom and allows flowing claims from the external identity
+ ///
+ public override async Task CreateAsync(BackOfficeIdentityUser user)
{
- if (user == null) throw new ArgumentNullException(nameof(user));
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
- var baseIdentity = await base.GenerateClaimsAsync(user);
+ ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user);
// now we can flow any custom claims that the actual user has currently assigned which could be done in the OnExternalLogin callback
- foreach (var claim in user.Claims)
+ foreach (IdentityUserClaim claim in user.Claims)
{
baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue));
}
-
+ // TODO: We want to remove UmbracoBackOfficeIdentity and only rely on ClaimsIdentity, once
+ // that is done then we'll create a ClaimsIdentity with all of the requirements here instead
var umbracoIdentity = new UmbracoBackOfficeIdentity(
baseIdentity,
user.Id,
@@ -43,7 +60,8 @@ namespace Umbraco.Core.BackOffice
return new ClaimsPrincipal(umbracoIdentity);
}
- protected override async Task GenerateClaimsAsync(TUser user)
+ ///
+ protected override async Task GenerateClaimsAsync(BackOfficeIdentityUser user)
{
// TODO: Have a look at the base implementation https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L79
// since it's setting an authentication type that is probably not what we want.
@@ -51,7 +69,7 @@ namespace Umbraco.Core.BackOffice
// the method above just returns a principal that wraps the identity and we dont use a custom principal,
// see https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L66
- var identity = await base.GenerateClaimsAsync(user);
+ ClaimsIdentity identity = await base.GenerateClaimsAsync(user);
return identity;
}
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs
similarity index 69%
rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs
rename to src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs
index 5bae03cad6..c9f8d35ada 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs
@@ -1,18 +1,25 @@
-using System;
+using System;
using System.Reflection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
-using Umbraco.Core.BackOffice;
-namespace Umbraco.Infrastructure.BackOffice
+namespace Umbraco.Core.Security
{
public class BackOfficeIdentityBuilder : IdentityBuilder
{
- public BackOfficeIdentityBuilder(IServiceCollection services) : base(typeof(BackOfficeIdentityUser), services)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BackOfficeIdentityBuilder(IServiceCollection services)
+ : base(typeof(BackOfficeIdentityUser), services)
{
}
- public BackOfficeIdentityBuilder(Type role, IServiceCollection services) : base(typeof(BackOfficeIdentityUser), role, services)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BackOfficeIdentityBuilder(Type role, IServiceCollection services)
+ : base(typeof(BackOfficeIdentityUser), role, services)
{
}
@@ -28,10 +35,8 @@ namespace Umbraco.Infrastructure.BackOffice
{
throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}");
}
- Services.Configure(options =>
- {
- options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider);
- });
+
+ Services.Configure(options => options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider));
Services.AddTransient(provider);
return this;
}
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityErrorDescriber.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs
similarity index 55%
rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityErrorDescriber.cs
rename to src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs
index 012ac5650f..6d36e489b8 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityErrorDescriber.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs
@@ -1,11 +1,12 @@
-using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
///
/// Umbraco back office specific
///
public class BackOfficeIdentityErrorDescriber : IdentityErrorDescriber
{
+ // TODO: Override all the methods in order to provide our own translated error messages
}
}
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityOptions.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs
similarity index 72%
rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityOptions.cs
rename to src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs
index 2f729072a6..77849c4d0c 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityOptions.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs
@@ -1,6 +1,6 @@
-using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
///
/// Identity options specifically for the back office identity implementation
diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs
new file mode 100644
index 0000000000..e2e8031768
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Identity;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Models.Entities;
+using Umbraco.Core.Models.Identity;
+using Umbraco.Core.Models.Membership;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// The identity user used for the back office
+ ///
+ public class BackOfficeIdentityUser : UmbracoIdentityUser
+ {
+ private string _name;
+ private string _passwordConfig;
+ private string _culture;
+ private IReadOnlyCollection _groups;
+ private string[] _allowedSections;
+ private int[] _startMediaIds;
+ private int[] _startContentIds;
+
+ // Custom comparer for enumerables
+ private static readonly DelegateEqualityComparer> s_groupsComparer = new DelegateEqualityComparer>(
+ (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)),
+ groups => groups.GetHashCode());
+
+ private static readonly DelegateEqualityComparer s_startIdsComparer = new DelegateEqualityComparer(
+ (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable),
+ groups => groups.GetHashCode());
+
+ ///
+ /// Used to construct a new instance without an identity
+ ///
+ /// This is allowed to be null (but would need to be filled in if trying to persist this instance)
+ public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string username, string email, string culture, string name = null)
+ {
+ if (string.IsNullOrWhiteSpace(username))
+ {
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
+ }
+
+ if (string.IsNullOrWhiteSpace(culture))
+ {
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
+ }
+
+ var user = new BackOfficeIdentityUser(globalSettings, Array.Empty());
+ user.DisableChangeTracking();
+ user.UserName = username;
+ user.Email = email;
+
+ user.Id = null;
+ user.HasIdentity = false;
+ user._culture = culture;
+ user._name = name;
+ user.EnableChangeTracking();
+ return user;
+ }
+
+ private BackOfficeIdentityUser(GlobalSettings globalSettings, IReadOnlyCollection groups)
+ {
+ _startMediaIds = Array.Empty();
+ _startContentIds = Array.Empty();
+ _allowedSections = Array.Empty();
+ _culture = globalSettings.DefaultUILanguage;
+
+ // use the property setters - they do more than just setting a field
+ Groups = groups;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BackOfficeIdentityUser(GlobalSettings globalSettings, int userId, IEnumerable groups)
+ : this(globalSettings, groups.ToArray())
+ {
+ // use the property setters - they do more than just setting a field
+ Id = UserIdToString(userId);
+ }
+
+ public int[] CalculatedMediaStartNodeIds { get; set; }
+ public int[] CalculatedContentStartNodeIds { get; set; }
+
+ ///
+ /// Gets or sets the user's real name
+ ///
+ public string Name
+ {
+ get => _name;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
+ }
+
+
+ public string PasswordConfig
+ {
+ get => _passwordConfig;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
+ }
+
+
+ ///
+ /// Gets or sets content start nodes assigned to the User (not ones assigned to the user's groups)
+ ///
+ public int[] StartContentIds
+ {
+ get => _startContentIds;
+ set
+ {
+ if (value == null)
+ {
+ value = new int[0];
+ }
+
+ BeingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), s_startIdsComparer);
+ }
+ }
+
+ ///
+ /// Gets or sets media start nodes assigned to the User (not ones assigned to the user's groups)
+ ///
+ public int[] StartMediaIds
+ {
+ get => _startMediaIds;
+ set
+ {
+ if (value == null)
+ {
+ value = new int[0];
+ }
+
+ BeingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), s_startIdsComparer);
+ }
+ }
+
+ ///
+ /// Gets a readonly list of the user's allowed sections which are based on it's user groups
+ ///
+ public string[] AllowedSections => _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray());
+
+ ///
+ /// Gets or sets the culture
+ ///
+ public string Culture
+ {
+ get => _culture;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture));
+ }
+
+ ///
+ /// Gets or sets the user groups
+ ///
+ public IReadOnlyCollection Groups
+ {
+ get => _groups;
+ set
+ {
+ // so they recalculate
+ _allowedSections = null;
+
+ _groups = value.Where(x => x.Alias != null).ToArray();
+
+ var roles = new List>();
+ foreach (IdentityUserRole identityUserRole in _groups.Select(x => new IdentityUserRole
+ {
+ RoleId = x.Alias,
+ UserId = Id?.ToString()
+ }))
+ {
+ roles.Add(identityUserRole);
+ }
+
+ // now reset the collection
+ Roles = roles;
+
+ BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer);
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the user is locked out based on the user's lockout end date
+ ///
+ public bool IsLockedOut
+ {
+ get
+ {
+ var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
+ return isLocked;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the IUser IsApproved
+ ///
+ public bool IsApproved { get; set; }
+
+ private static string UserIdToString(int userId) => string.Intern(userId.ToString());
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizer.cs b/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs
similarity index 75%
rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizer.cs
rename to src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs
index cc9249d462..957e36d1d0 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizer.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs
@@ -1,6 +1,6 @@
-using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
///
@@ -8,6 +8,8 @@ namespace Umbraco.Core.BackOffice
///
public class BackOfficeLookupNormalizer : ILookupNormalizer
{
+ // TODO: Do we need this?
+
public string NormalizeName(string name) => name;
public string NormalizeEmail(string email) => email;
diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
new file mode 100644
index 0000000000..1756e84d76
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
@@ -0,0 +1,777 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Options;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Mapping;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Identity;
+using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Scoping;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Core.Security
+{
+ // TODO: Make this into a base class that can be re-used
+
+ ///
+ /// The user store for back office users
+ ///
+ public class BackOfficeUserStore : UserStoreBase, string, IdentityUserClaim, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim>
+ {
+ private readonly IScopeProvider _scopeProvider;
+ private readonly IUserService _userService;
+ private readonly IEntityService _entityService;
+ private readonly IExternalLoginService _externalLoginService;
+ private readonly GlobalSettings _globalSettings;
+ private readonly UmbracoMapper _mapper;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BackOfficeUserStore(
+ IScopeProvider scopeProvider,
+ IUserService userService,
+ IEntityService entityService,
+ IExternalLoginService externalLoginService,
+ IOptions globalSettings,
+ UmbracoMapper mapper,
+ IdentityErrorDescriber describer)
+ : base(describer)
+ {
+ _scopeProvider = scopeProvider;
+ _userService = userService ?? throw new ArgumentNullException(nameof(userService));
+ _entityService = entityService;
+ _externalLoginService = externalLoginService ?? throw new ArgumentNullException(nameof(externalLoginService));
+ _globalSettings = globalSettings.Value;
+ _mapper = mapper;
+ _userService = userService;
+ _externalLoginService = externalLoginService;
+ }
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override IQueryable Users => throw new NotImplementedException();
+
+ ///
+ public override Task GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken);
+
+ ///
+ public override Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken);
+
+ ///
+ public override Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ // the password must be 'something' it could be empty if authenticating
+ // with an external provider so we'll just generate one and prefix it, the
+ // prefix will help us determine if the password hasn't actually been specified yet.
+ // this will hash the guid with a salt so should be nicely random
+ var aspHasher = new PasswordHasher();
+ var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix +
+ aspHasher.HashPassword(user, Guid.NewGuid().ToString("N"));
+
+ var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue)
+ {
+ Language = user.Culture ?? _globalSettings.DefaultUILanguage,
+ StartContentIds = user.StartContentIds ?? new int[] { },
+ StartMediaIds = user.StartMediaIds ?? new int[] { },
+ IsLockedOut = user.IsLockedOut,
+ };
+
+ // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
+ var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
+
+ UpdateMemberProperties(userEntity, user);
+
+ _userService.Save(userEntity);
+
+ if (!userEntity.HasIdentity)
+ {
+ throw new DataException("Could not create the user, check logs for details");
+ }
+
+ // re-assign id
+ user.Id = UserIdToString(userEntity.Id);
+
+ if (isLoginsPropertyDirty)
+ {
+ _externalLoginService.Save(
+ userEntity.Id,
+ user.Logins.Select(x => new ExternalLogin(
+ x.LoginProvider,
+ x.ProviderKey,
+ x.UserData)));
+ }
+
+ return Task.FromResult(IdentityResult.Success);
+ }
+
+ ///
+ public override Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ Attempt asInt = user.Id.TryConvertTo();
+ if (asInt == false)
+ {
+ throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
+ }
+
+ using (IScope scope = _scopeProvider.CreateScope())
+ {
+ IUser found = _userService.GetUserById(asInt.Result);
+ if (found != null)
+ {
+ // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
+ var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
+
+ if (UpdateMemberProperties(found, user))
+ {
+ _userService.Save(found);
+ }
+
+ if (isLoginsPropertyDirty)
+ {
+ _externalLoginService.Save(
+ found.Id,
+ user.Logins.Select(x => new ExternalLogin(
+ x.LoginProvider,
+ x.ProviderKey,
+ x.UserData)));
+ }
+ }
+
+ scope.Complete();
+ }
+
+ return Task.FromResult(IdentityResult.Success);
+ }
+
+ ///
+ public override Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ IUser found = _userService.GetUserById(UserIdToInt(user.Id));
+ if (found != null)
+ {
+ _userService.Delete(found);
+ }
+
+ _externalLoginService.DeleteUserLogins(UserIdToInt(user.Id));
+
+ return Task.FromResult(IdentityResult.Success);
+ }
+
+ ///
+ public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
+
+ ///
+ protected override Task FindUserAsync(string userId, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+
+ IUser user = _userService.GetUserById(UserIdToInt(userId));
+ if (user == null)
+ {
+ return Task.FromResult((BackOfficeIdentityUser)null);
+ }
+
+ return Task.FromResult(AssignLoginsCallback(_mapper.Map(user)));
+ }
+
+ ///
+ public override Task FindByNameAsync(string userName, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ IUser user = _userService.GetByUsername(userName);
+ if (user == null)
+ {
+ return Task.FromResult((BackOfficeIdentityUser)null);
+ }
+
+ BackOfficeIdentityUser result = AssignLoginsCallback(_mapper.Map(user));
+
+ return Task.FromResult(result);
+ }
+
+ ///
+ public override async Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default)
+ {
+ await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
+
+ user.PasswordConfig = null; // Clear this so that it's reset at the repository level
+ user.LastPasswordChangeDateUtc = DateTime.UtcNow;
+ }
+
+ ///
+ public override async Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ // This checks if it's null
+ var result = await base.HasPasswordAsync(user, cancellationToken);
+ if (result)
+ {
+ // we also want to check empty
+ return string.IsNullOrEmpty(user.PasswordHash) == false;
+ }
+
+ return result;
+ }
+
+ ///
+ public override Task FindByEmailAsync(string email, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ IUser user = _userService.GetByEmail(email);
+ BackOfficeIdentityUser result = user == null
+ ? null
+ : _mapper.Map(user);
+
+ return Task.FromResult(AssignLoginsCallback(result));
+ }
+
+ ///
+ public override Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
+ => GetEmailAsync(user, cancellationToken);
+
+ ///
+ public override Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
+ => SetEmailAsync(user, normalizedEmail, cancellationToken);
+
+ ///
+ public override Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ if (login == null)
+ {
+ throw new ArgumentNullException(nameof(login));
+ }
+
+ ICollection logins = user.Logins;
+ var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString());
+ IdentityUserLogin userLogin = instance;
+ logins.Add(userLogin);
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ IIdentityUserLogin userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
+ if (userLogin != null)
+ {
+ user.Logins.Remove(userLogin);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ return Task.FromResult((IList)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList());
+ }
+
+ ///
+ protected override async Task> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+
+ BackOfficeIdentityUser user = await FindUserAsync(userId, cancellationToken);
+ if (user == null)
+ {
+ return null;
+ }
+
+ IList logins = await GetLoginsAsync(user, cancellationToken);
+ UserLoginInfo found = logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider);
+ if (found == null)
+ {
+ return null;
+ }
+
+ return new IdentityUserLogin
+ {
+ LoginProvider = found.LoginProvider,
+ ProviderKey = found.ProviderKey,
+ ProviderDisplayName = found.ProviderDisplayName, // TODO: We don't store this value so it will be null
+ UserId = user.Id
+ };
+ }
+
+ ///
+ protected override Task> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+
+ var logins = _externalLoginService.Find(loginProvider, providerKey).ToList();
+ if (logins.Count == 0)
+ {
+ return Task.FromResult((IdentityUserLogin)null);
+ }
+
+ IIdentityUserLogin found = logins[0];
+ return Task.FromResult(new IdentityUserLogin
+ {
+ LoginProvider = found.LoginProvider,
+ ProviderKey = found.ProviderKey,
+ ProviderDisplayName = null, // TODO: We don't store this value so it will be null
+ UserId = found.UserId
+ });
+ }
+
+ ///
+ /// Adds a user to a role (user group)
+ ///
+ public override Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ if (normalizedRoleName == null)
+ {
+ throw new ArgumentNullException(nameof(normalizedRoleName));
+ }
+
+ if (string.IsNullOrWhiteSpace(normalizedRoleName))
+ {
+ throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
+ }
+
+ IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
+
+ if (userRole == null)
+ {
+ user.AddRole(normalizedRoleName);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Removes the role (user group) for the user
+ ///
+ public override Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ if (normalizedRoleName == null)
+ {
+ throw new ArgumentNullException(nameof(normalizedRoleName));
+ }
+
+ if (string.IsNullOrWhiteSpace(normalizedRoleName))
+ {
+ throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
+ }
+
+ IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
+
+ if (userRole != null)
+ {
+ user.Roles.Remove(userRole);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Returns the roles (user groups) for this user
+ ///
+ public override Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList());
+ }
+
+ ///
+ /// Returns true if a user is in the role
+ ///
+ public override Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName));
+ }
+
+ ///
+ /// Lists all users of a given role.
+ ///
+ ///
+ /// Identity Role names are equal to Umbraco UserGroup alias.
+ ///
+ public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (normalizedRoleName == null)
+ {
+ throw new ArgumentNullException(nameof(normalizedRoleName));
+ }
+
+ IUserGroup userGroup = _userService.GetUserGroupByAlias(normalizedRoleName);
+
+ IEnumerable users = _userService.GetAllInGroup(userGroup.Id);
+ IList backOfficeIdentityUsers = users.Select(x => _mapper.Map(x)).ToList();
+
+ return Task.FromResult(backOfficeIdentityUsers);
+ }
+
+ ///
+ protected override Task> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken)
+ {
+ IUserGroup group = _userService.GetUserGroupByAlias(normalizedRoleName);
+ if (group == null)
+ {
+ return Task.FromResult((IdentityRole)null);
+ }
+
+ return Task.FromResult(new IdentityRole(group.Name)
+ {
+ Id = group.Alias
+ });
+ }
+
+ ///
+ protected override async Task> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken)
+ {
+ BackOfficeIdentityUser user = await FindUserAsync(userId, cancellationToken);
+ if (user == null)
+ {
+ return null;
+ }
+
+ IdentityUserRole found = user.Roles.FirstOrDefault(x => x.RoleId.InvariantEquals(roleId));
+ return found;
+ }
+
+ ///
+ public override Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ // the stamp cannot be null, so if it is currently null then we'll just return a hash of the password
+ return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace()
+ ? user.PasswordHash.GenerateHash()
+ : user.SecurityStamp);
+ }
+
+ private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user)
+ {
+ if (user != null)
+ {
+ user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id))));
+ }
+
+ return user;
+ }
+
+ private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser)
+ {
+ var anythingChanged = false;
+
+ // don't assign anything if nothing has changed as this will trigger the track changes of the model
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastLoginDateUtc))
+ || (user.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false)
+ || (identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value))
+ {
+ anythingChanged = true;
+
+ // if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
+ DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime();
+ user.LastLoginDate = dt;
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc))
+ || (user.LastPasswordChangeDate != default && identityUser.LastPasswordChangeDateUtc.HasValue == false)
+ || (identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value))
+ {
+ anythingChanged = true;
+ user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime();
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.EmailConfirmed))
+ || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default && identityUser.EmailConfirmed == false)
+ || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default) && identityUser.EmailConfirmed))
+ {
+ anythingChanged = true;
+ user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null;
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name))
+ && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false)
+ {
+ anythingChanged = true;
+ user.Name = identityUser.Name;
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Email))
+ && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false)
+ {
+ anythingChanged = true;
+ user.Email = identityUser.Email;
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.AccessFailedCount))
+ && user.FailedPasswordAttempts != identityUser.AccessFailedCount)
+ {
+ anythingChanged = true;
+ user.FailedPasswordAttempts = identityUser.AccessFailedCount;
+ }
+
+ if (user.IsLockedOut != identityUser.IsLockedOut)
+ {
+ anythingChanged = true;
+ user.IsLockedOut = identityUser.IsLockedOut;
+
+ if (user.IsLockedOut)
+ {
+ // need to set the last lockout date
+ user.LastLockoutDate = DateTime.Now;
+ }
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName))
+ && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false)
+ {
+ anythingChanged = true;
+ user.Username = identityUser.UserName;
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash))
+ && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false)
+ {
+ anythingChanged = true;
+ user.RawPasswordValue = identityUser.PasswordHash;
+ user.PasswordConfiguration = identityUser.PasswordConfig;
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Culture))
+ && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false)
+ {
+ anythingChanged = true;
+ user.Language = identityUser.Culture;
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartMediaIds))
+ && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false)
+ {
+ anythingChanged = true;
+ user.StartMediaIds = identityUser.StartMediaIds;
+ }
+
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartContentIds))
+ && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false)
+ {
+ anythingChanged = true;
+ user.StartContentIds = identityUser.StartContentIds;
+ }
+
+ if (user.SecurityStamp != identityUser.SecurityStamp)
+ {
+ anythingChanged = true;
+ user.SecurityStamp = identityUser.SecurityStamp;
+ }
+
+ // TODO: Fix this for Groups too
+ if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups)))
+ {
+ var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray();
+
+ var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray();
+ var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray();
+
+ var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray();
+
+ if (userGroupAliases.ContainsAll(combinedAliases) == false
+ || combinedAliases.ContainsAll(userGroupAliases) == false)
+ {
+ anythingChanged = true;
+
+ // clear out the current groups (need to ToArray since we are modifying the iterator)
+ user.ClearGroups();
+
+ // go lookup all these groups
+ var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray();
+
+ // use all of the ones assigned and add them
+ foreach (var group in groups)
+ {
+ user.AddGroup(group);
+ }
+
+ // re-assign
+ identityUser.Groups = groups;
+ }
+ }
+
+ // we should re-set the calculated start nodes
+ identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService);
+ identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService);
+
+ // reset all changes
+ identityUser.ResetDirtyProperties(false);
+
+ return anythingChanged;
+ }
+
+ ///
+ public Task ValidateSessionIdAsync(string userId, string sessionId)
+ {
+ if (Guid.TryParse(sessionId, out Guid guidSessionId))
+ {
+ return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId));
+ }
+
+ return Task.FromResult(false);
+ }
+
+ private static int UserIdToInt(string userId)
+ {
+ Attempt attempt = userId.TryConvertTo();
+ if (attempt.Success)
+ {
+ return attempt.Result;
+ }
+
+ throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception);
+ }
+
+ private static string UserIdToString(int userId) => string.Intern(userId.ToString());
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task> GetClaimsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task AddClaimsAsync(BackOfficeIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task ReplaceClaimAsync(BackOfficeIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task RemoveClaimsAsync(BackOfficeIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ // TODO: We should support these
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ protected override Task> FindTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ protected override Task AddUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ protected override Task RemoveUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException();
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs
similarity index 87%
rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs
rename to src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs
index 131bd08ac9..8b2c8932a7 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs
@@ -1,7 +1,8 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
+using Umbraco.Core.Security;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
public class BackOfficeUserValidator : UserValidator
where T : BackOfficeIdentityUser
diff --git a/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs
new file mode 100644
index 0000000000..4235195bb1
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs
@@ -0,0 +1,11 @@
+using Umbraco.Core.Security;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// The user manager for the back office
+ ///
+ public interface IBackOfficeUserManager : IUmbracoUserManager
+ {
+ }
+}
diff --git a/src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs
similarity index 83%
rename from src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs
rename to src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs
index 5874337f4a..fdf1f1fcf2 100644
--- a/src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs
+++ b/src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs
@@ -1,6 +1,6 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
///
/// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily
@@ -11,9 +11,6 @@ namespace Umbraco.Core.BackOffice
///
/// Checks a password for a user
///
- ///
- ///
- ///
///
/// This will allow a developer to auto-link a local account which is required if the user queried doesn't exist locally.
/// The user parameter will always contain the username, if the user doesn't exist locally, the other properties will not be filled in.
diff --git a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs
similarity index 85%
rename from src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs
rename to src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs
index c026c256f5..4bec4c9c7a 100644
--- a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs
+++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs
@@ -5,27 +5,56 @@ using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Security;
using Umbraco.Web.Models.ContentEditing;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
- public interface IBackOfficeUserManager : IBackOfficeUserManager
- {
- }
- public interface IBackOfficeUserManager: IDisposable
+ ///
+ /// A user manager for Umbraco (either back office users or front-end members)
+ ///
+ /// The type of user
+ public interface IUmbracoUserManager : IDisposable
where TUser : BackOfficeIdentityUser
{
+ ///
+ /// Gets the user id of a user
+ ///
+ /// The user
+ /// A representing the result of the asynchronous operation.
Task GetUserIdAsync(TUser user);
+ ///
+ /// Get the from a
+ ///
+ /// The
+ /// A representing the result of the asynchronous operation.
Task GetUserAsync(ClaimsPrincipal principal);
+ ///
+ /// Get the user id from the
+ ///
+ /// the
+ /// Returns the user id from the
string GetUserId(ClaimsPrincipal principal);
+ ///
+ /// Gets the external logins for the user
+ ///
+ /// A representing the result of the asynchronous operation.
Task> GetLoginsAsync(TUser user);
+ ///
+ /// Deletes a user
+ ///
+ /// A representing the result of the asynchronous operation.
Task DeleteAsync(TUser user);
+ ///
+ /// Finds a user by the external login provider
+ ///
+ /// A representing the result of the asynchronous operation.
Task FindByLoginAsync(string loginProvider, string providerKey);
///
@@ -49,15 +78,11 @@ namespace Umbraco.Core.BackOffice
///
/// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
///
- ///
- ///
- ///
- ///
///
/// We use this because in the back office the only way an admin can change another user's password without first knowing their password
/// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset
///
- Task ChangePasswordWithResetAsync(int userId, string token, string newPassword);
+ Task ChangePasswordWithResetAsync(string userId, string token, string newPassword);
///
/// Validates that an email confirmation token matches the specified .
@@ -97,8 +122,6 @@ namespace Umbraco.Core.BackOffice
///
/// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date
///
- ///
- ///
///
/// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values
///
@@ -145,8 +168,7 @@ namespace Umbraco.Core.BackOffice
/// The that represents the asynchronous operation, returning true if the
/// is valid, otherwise false.
///
- Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose,
- string token);
+ Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, string token);
///
/// Adds the to the specified only if the user
@@ -160,7 +182,6 @@ namespace Umbraco.Core.BackOffice
///
Task AddPasswordAsync(TUser user, string password);
-
///
/// Returns a flag indicating whether the given is valid for the
/// specified .
@@ -183,15 +204,12 @@ namespace Umbraco.Core.BackOffice
/// The that represents the asynchronous operation, containing the
/// of the operation.
///
- Task ChangePasswordAsync(TUser user, string currentPassword,
- string newPassword);
+ Task ChangePasswordAsync(TUser user, string currentPassword, string newPassword);
///
/// Used to validate a user's session
///
- ///
- ///
- ///
+ /// Returns true if the session is valid, otherwise false
Task ValidateSessionIdAsync(string userId, string sessionId);
///
@@ -206,12 +224,11 @@ namespace Umbraco.Core.BackOffice
Task CreateAsync(TUser user);
///
- /// Helper method to generate a password for a user based on the current password validator
+ /// Generate a password for a user based on the current password validator
///
- ///
+ /// A generated password
string GeneratePassword();
-
///
/// Generates an email confirmation token for the specified user.
///
@@ -290,8 +307,16 @@ namespace Umbraco.Core.BackOffice
/// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the Microsoft.AspNetCore.Identity.IdentityResult of the operation.
Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey);
+ ///
+ /// Resets the access failed count for the user
+ ///
+ /// A representing the result of the asynchronous operation.
Task ResetAccessFailedCountAsync(TUser user);
+ ///
+ /// Generates a two factor token for the user
+ ///
+ /// A representing the result of the asynchronous operation.
Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider);
///
@@ -314,9 +339,10 @@ namespace Umbraco.Core.BackOffice
// TODO: These are raised from outside the signinmanager and usermanager in the auth and user controllers,
// let's see if there's a way to avoid that and only have these called within signinmanager and usermanager
// which means we can remove these from the interface (things like invite seems like they cannot be moved)
- void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId);
- void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId);
- SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId);
+ // TODO: When we change to not having the crappy static events this will need to be revisited
+ void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId);
+ void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId);
+ SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId);
UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser);
bool HasSendingUserInviteEventHandler { get; }
diff --git a/src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs
similarity index 55%
rename from src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs
rename to src/Umbraco.Infrastructure/Security/IUserSessionStore.cs
index 69d5408cf7..c68d1f13f9 100644
--- a/src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs
+++ b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs
@@ -1,15 +1,17 @@
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Identity;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
///
/// An IUserStore interface part to implement if the store supports validating user session Ids
///
- ///
- public interface IUserSessionStore : IUserStore
+ /// The user type
+ public interface IUserSessionStore
where TUser : class
{
+ ///
+ /// Validates a user's session is still valid
+ ///
Task ValidateSessionIdAsync(string userId, string sessionId);
}
}
diff --git a/src/Umbraco.Infrastructure/BackOffice/IdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs
similarity index 100%
rename from src/Umbraco.Infrastructure/BackOffice/IdentityExtensions.cs
rename to src/Umbraco.Infrastructure/Security/IdentityExtensions.cs
diff --git a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs
similarity index 95%
rename from src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs
rename to src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs
index 61fdf82d19..aebb2de5bf 100644
--- a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs
+++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
@@ -7,7 +7,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
public class IdentityMapDefinition : IMapDefinition
{
@@ -65,7 +65,7 @@ namespace Umbraco.Core.BackOffice
target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); // project CultureInfo to string
target.IsApproved = source.IsApproved;
target.SecurityStamp = source.SecurityStamp;
- target.LockoutEndDateUtc = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?) null;
+ target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null;
// this was in AutoMapper but does not have a setter anyways
//target.AllowedSections = source.AllowedSections.ToArray(),
diff --git a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs
index 961c2e6137..626932640c 100644
--- a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs
+++ b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs
@@ -1,4 +1,6 @@
-namespace Umbraco.Core.BackOffice
+using Umbraco.Core.Security;
+
+namespace Umbraco.Core.Security
{
///
@@ -6,7 +8,7 @@
///
public class SignOutAuditEventArgs : IdentityAuditEventArgs
{
- public SignOutAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, int performingUser = -1, int affectedUser = -1)
+ public SignOutAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, string performingUser = Constants.Security.SuperUserIdAsString, string affectedUser = Constants.Security.SuperUserIdAsString)
: base(action, ipAddress, performingUser, comment, affectedUser, null)
{
}
diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs
new file mode 100644
index 0000000000..1b888123be
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs
@@ -0,0 +1,276 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using Microsoft.AspNetCore.Identity;
+using Umbraco.Core.Models.Entities;
+
+namespace Umbraco.Core.Models.Identity
+{
+
+ ///
+ /// Abstract class for use in Umbraco Identity for users and members
+ ///
+ ///
+ ///
+ /// This uses strings for the ID of the user, claims, roles. This is because aspnetcore identity's base store will
+ /// not support having an INT user PK and a string role PK with the way they've made the generics. So we will just use
+ /// string for both which makes things more flexible anyways for users and members and also if/when we transition to
+ /// GUID support
+ ///
+ ///
+ /// This class was originally borrowed from the EF implementation in Identity prior to netcore.
+ /// The new IdentityUser in netcore does not have properties such as Claims, Roles and Logins and those are instead
+ /// by default managed with their default user store backed by EF which utilizes EF's change tracking to track these values
+ /// to a user. We will continue using this approach since it works fine for what we need which does the change tracking of
+ /// claims, roles and logins directly on the user model.
+ ///
+ ///
+ public abstract class UmbracoIdentityUser : IdentityUser, IRememberBeingDirty
+ {
+ private string _id;
+ private string _email;
+ private string _userName;
+ private DateTime? _lastLoginDateUtc;
+ private bool _emailConfirmed;
+ private int _accessFailedCount;
+ private string _passwordHash;
+ private DateTime? _lastPasswordChangeDateUtc;
+ private ObservableCollection _logins;
+ private Lazy> _getLogins;
+ private ObservableCollection> _roles;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UmbracoIdentityUser()
+ {
+ // must initialize before setting groups
+ _roles = new ObservableCollection>();
+ _roles.CollectionChanged += Roles_CollectionChanged;
+ Claims = new List>();
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged
+ {
+ add
+ {
+ BeingDirty.PropertyChanged += value;
+ }
+
+ remove
+ {
+ BeingDirty.PropertyChanged -= value;
+ }
+ }
+
+ ///
+ /// Gets or sets last login date
+ ///
+ public DateTime? LastLoginDateUtc
+ {
+ get => _lastLoginDateUtc;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, nameof(LastLoginDateUtc));
+ }
+
+ ///
+ /// Gets or sets email
+ ///
+ public override string Email
+ {
+ get => _email;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email));
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the email is confirmed, default is false
+ ///
+ public override bool EmailConfirmed
+ {
+ get => _emailConfirmed;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, nameof(EmailConfirmed));
+ }
+
+ ///
+ /// Gets or sets the salted/hashed form of the user password
+ ///
+ public override string PasswordHash
+ {
+ get => _passwordHash;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash));
+ }
+
+ ///
+ /// Gets or sets dateTime in UTC when the password was last changed.
+ ///
+ public DateTime? LastPasswordChangeDateUtc
+ {
+ get => _lastPasswordChangeDateUtc;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDateUtc, nameof(LastPasswordChangeDateUtc));
+ }
+
+ ///
+ /// Gets or sets a value indicating whether is lockout enabled for this user
+ ///
+ ///
+ /// Currently this is always true for users and members
+ ///
+ public override bool LockoutEnabled
+ {
+ get => true;
+ set { }
+ }
+
+ ///
+ /// Gets or sets the value to record failures for the purposes of lockout
+ ///
+ public override int AccessFailedCount
+ {
+ get => _accessFailedCount;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, nameof(AccessFailedCount));
+ }
+
+ ///
+ /// Gets or sets the user roles collection
+ ///
+ public ICollection> Roles
+ {
+ get => _roles;
+ set
+ {
+ _roles.CollectionChanged -= Roles_CollectionChanged;
+ _roles = new ObservableCollection>(value);
+ _roles.CollectionChanged += Roles_CollectionChanged;
+ }
+ }
+
+ ///
+ /// Gets navigation the user claims collection
+ ///
+ public ICollection> Claims { get; }
+
+ ///
+ /// Gets the user logins collection
+ ///
+ public ICollection Logins
+ {
+ get
+ {
+ // return if it exists
+ if (_logins != null)
+ {
+ return _logins;
+ }
+
+ _logins = new ObservableCollection();
+
+ // if the callback is there and hasn't been created yet then execute it and populate the logins
+ if (_getLogins != null && !_getLogins.IsValueCreated)
+ {
+ foreach (IIdentityUserLogin l in _getLogins.Value)
+ {
+ _logins.Add(l);
+ }
+ }
+
+ // now assign events
+ _logins.CollectionChanged += Logins_CollectionChanged;
+
+ return _logins;
+ }
+ }
+
+ ///
+ /// Gets or sets user ID (Primary Key)
+ ///
+ public override string Id
+ {
+ get => _id;
+ set
+ {
+ _id = value;
+ HasIdentity = true;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether returns an Id has been set on this object this will be false if the object is new and not persisted to the database
+ ///
+ public bool HasIdentity { get; protected set; }
+
+ ///
+ /// Gets or sets user name
+ ///
+ public override string UserName
+ {
+ get => _userName;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _userName, nameof(UserName));
+ }
+
+ ///
+ /// Gets the for change tracking
+ ///
+ protected BeingDirty BeingDirty { get; } = new BeingDirty();
+
+ ///
+ public bool IsDirty() => BeingDirty.IsDirty();
+
+ ///
+ public bool IsPropertyDirty(string propName) => BeingDirty.IsPropertyDirty(propName);
+
+ ///
+ public IEnumerable GetDirtyProperties() => BeingDirty.GetDirtyProperties();
+
+ ///
+ public void ResetDirtyProperties() => BeingDirty.ResetDirtyProperties();
+
+ ///
+ public bool WasDirty() => BeingDirty.WasDirty();
+
+ ///
+ public bool WasPropertyDirty(string propertyName) => BeingDirty.WasPropertyDirty(propertyName);
+
+ ///
+ public void ResetWereDirtyProperties() => BeingDirty.ResetWereDirtyProperties();
+
+ ///
+ public void ResetDirtyProperties(bool rememberDirty) => BeingDirty.ResetDirtyProperties(rememberDirty);
+
+ ///
+ public IEnumerable GetWereDirtyProperties() => BeingDirty.GetWereDirtyProperties();
+
+ ///
+ /// Disables change tracking.
+ ///
+ public void DisableChangeTracking() => BeingDirty.DisableChangeTracking();
+
+ ///
+ /// Enables change tracking.
+ ///
+ public void EnableChangeTracking() => BeingDirty.EnableChangeTracking();
+
+ ///
+ /// Adds a role
+ ///
+ /// The role to add
+ ///
+ /// Adding a role this way will not reflect on the user's group's collection or it's allowed sections until the user is persisted
+ ///
+ public void AddRole(string role) => Roles.Add(new IdentityUserRole
+ {
+ UserId = Id,
+ RoleId = role
+ });
+
+ ///
+ /// Used to set a lazy call back to populate the user's Login list
+ ///
+ /// The lazy value
+ public void SetLoginsCallback(Lazy> callback) => _getLogins = callback ?? throw new ArgumentNullException(nameof(callback));
+
+ private void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => BeingDirty.OnPropertyChanged(nameof(Logins));
+
+ private void Roles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => BeingDirty.OnPropertyChanged(nameof(Roles));
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs
new file mode 100644
index 0000000000..6318218669
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Models.Identity;
+using Umbraco.Net;
+
+namespace Umbraco.Core.Security
+{
+
+ ///
+ /// Abstract class for Umbraco User Managers for back office users or front-end members
+ ///
+ /// The type of user
+ /// /// The type password config
+ public abstract class UmbracoUserManager : UserManager
+ where TUser : UmbracoIdentityUser
+ where TPasswordConfig : class, IPasswordConfiguration, new()
+ {
+ private PasswordGenerator _passwordGenerator;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UmbracoUserManager(
+ IIpResolver ipResolver,
+ IUserStore store,
+ IOptions optionsAccessor,
+ IPasswordHasher passwordHasher,
+ IEnumerable> userValidators,
+ IEnumerable> passwordValidators,
+ ILookupNormalizer keyNormalizer,
+ IdentityErrorDescriber errors,
+ IServiceProvider services,
+ ILogger> logger,
+ IOptions passwordConfiguration)
+ : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
+ {
+ IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver));
+ PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
+ }
+
+ ///
+ public override bool SupportsUserClaim => false; // We don't support an IUserClaimStore and don't need to (at least currently)
+
+ ///
+ public override bool SupportsQueryableUsers => false; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository
+
+ ///
+ /// Developers will need to override this to support custom 2 factor auth
+ ///
+ ///
+ public override bool SupportsUserTwoFactor => false;
+
+ ///
+ public override bool SupportsUserPhoneNumber => false; // We haven't needed to support this yet, though might be necessary for 2FA
+
+ ///
+ /// Gets the password configuration
+ ///
+ public IPasswordConfiguration PasswordConfiguration { get; }
+
+ ///
+ /// Gets the IP resolver
+ ///
+ public IIpResolver IpResolver { get; }
+
+ ///
+ /// Used to validate a user's session
+ ///
+ /// The user id
+ /// The sesion id
+ /// True if the sesion is valid, else false
+ public virtual async Task ValidateSessionIdAsync(string userId, string sessionId)
+ {
+ var userSessionStore = Store as IUserSessionStore;
+
+ // if this is not set, for backwards compat (which would be super rare), we'll just approve it
+ // TODO: This should be removed after members supports this
+ if (userSessionStore == null)
+ {
+ return true;
+ }
+
+ return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
+ }
+
+ ///
+ /// This will determine which password hasher to use based on what is defined in config
+ ///
+ /// The
+ /// An
+ protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher();
+
+ ///
+ /// Helper method to generate a password for a user based on the current password validator
+ ///
+ /// The generated password
+ public string GeneratePassword()
+ {
+ if (_passwordGenerator == null)
+ {
+ _passwordGenerator = new PasswordGenerator(PasswordConfiguration);
+ }
+
+ var password = _passwordGenerator.GeneratePassword();
+ return password;
+ }
+
+ ///
+ public override async Task CheckPasswordAsync(TUser user, string password)
+ {
+ // we cannot proceed if the user passed in does not have an identity
+ if (user.HasIdentity == false)
+ {
+ return false;
+ }
+
+ // use the default behavior
+ return await base.CheckPasswordAsync(user, password);
+ }
+
+ ///
+ /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
+ ///
+ /// The userId
+ /// The reset password token
+ /// The new password to set it to
+ /// The
+ ///
+ /// We use this because in the back office the only way an admin can change another user's password without first knowing their password
+ /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset
+ ///
+ public virtual async Task ChangePasswordWithResetAsync(string userId, string token, string newPassword)
+ {
+ TUser user = await FindByIdAsync(userId);
+ if (user == null)
+ {
+ throw new InvalidOperationException("Could not find user");
+ }
+
+ IdentityResult result = await ResetPasswordAsync(user, token, newPassword);
+ return result;
+ }
+
+ ///
+ public override async Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd);
+
+ // The way we unlock is by setting the lockoutEnd date to the current datetime
+ if (!result.Succeeded || lockoutEnd < DateTimeOffset.UtcNow)
+ {
+ // Resets the login attempt fails back to 0 when unlock is clicked
+ await ResetAccessFailedCountAsync(user);
+ }
+
+ return result;
+ }
+
+ ///
+ public override async Task ResetAccessFailedCountAsync(TUser user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ var lockoutStore = (IUserLockoutStore)Store;
+ var accessFailedCount = await GetAccessFailedCountAsync(user);
+
+ if (accessFailedCount == 0)
+ {
+ return IdentityResult.Success;
+ }
+
+ await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None);
+
+ return await UpdateAsync(user);
+ }
+
+ ///
+ /// Overrides the Microsoft ASP.NET user management method
+ ///
+ ///
+ public override async Task AccessFailedAsync(TUser user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ var lockoutStore = Store as IUserLockoutStore;
+ if (lockoutStore == null)
+ {
+ throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>));
+ }
+
+ var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None);
+
+ if (count >= Options.Lockout.MaxFailedAccessAttempts)
+ {
+ await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), CancellationToken.None);
+
+ // NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0
+ // here we are persisting the value for the back office
+ }
+
+ IdentityResult result = await UpdateAsync(user);
+ return result;
+ }
+
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs
index 4e980b7bb1..80b05497a8 100644
--- a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs
+++ b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs
@@ -1,12 +1,13 @@
-using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Security;
using Umbraco.Web.Models.ContentEditing;
-namespace Umbraco.Core.BackOffice
+namespace Umbraco.Core.Security
{
public class UserInviteEventArgs : IdentityAuditEventArgs
{
- public UserInviteEventArgs(string ipAddress, int performingUser, UserInvite invitedUser, IUser localUser, string comment = null)
- : base(AuditEvent.SendingUserInvite, ipAddress, performingUser, comment, localUser.Id, localUser.Name)
+ public UserInviteEventArgs(string ipAddress, string performingUser, UserInvite invitedUser, IUser localUser, string comment = null)
+ : base(AuditEvent.SendingUserInvite, ipAddress, performingUser, comment, string.Intern(localUser.Id.ToString()), localUser.Name)
{
InvitedUser = invitedUser ?? throw new System.ArgumentNullException(nameof(invitedUser));
User = localUser;
@@ -24,7 +25,7 @@ namespace Umbraco.Core.BackOffice
///
/// The local user that has been created that is pending the invite
- ///
+ ///
public IUser User { get; }
///
diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs
index 3702a7e761..479e5cc4d8 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs
@@ -374,6 +374,7 @@ namespace Umbraco.Core.Services.Implement
new XElement("MandatoryMessage", propertyType.MandatoryMessage),
new XElement("Validation", propertyType.ValidationRegExp),
new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage),
+ new XElement("LabelOnTop", propertyType.LabelOnTop),
new XElement("Description", new XCData(propertyType.Description)));
genericProperties.Add(genericProperty);
}
@@ -501,6 +502,7 @@ namespace Umbraco.Core.Services.Implement
new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name),
new XElement("SortOrder", propertyType.SortOrder),
new XElement("Mandatory", propertyType.Mandatory.ToString()),
+ new XElement("LabelOnTop", propertyType.LabelOnTop.ToString()),
propertyType.MandatoryMessage != null ? new XElement("MandatoryMessage", propertyType.MandatoryMessage) : null,
propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null,
propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null,
diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs
index fabbfea1d4..5edbe77cdb 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
@@ -25,7 +25,8 @@ namespace Umbraco.Core.Services.Implement
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
- return _externalLoginRepository.Get(Query().Where(x => x.UserId == userId))
+ var asString = userId.ToString(); // TODO: This is temp until we update the external service to support guids for both users and members
+ return _externalLoginRepository.Get(Query().Where(x => x.UserId == asString))
.ToList();
}
}
diff --git a/src/Umbraco.Tests.Common/Builders/AuditEntryBuilder.cs b/src/Umbraco.Tests.Common/Builders/AuditEntryBuilder.cs
index 6efdc8aca8..447aab3fe9 100644
--- a/src/Umbraco.Tests.Common/Builders/AuditEntryBuilder.cs
+++ b/src/Umbraco.Tests.Common/Builders/AuditEntryBuilder.cs
@@ -1,5 +1,7 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
using System;
-using System.Globalization;
using Umbraco.Core.Models;
using Umbraco.Tests.Common.Builders.Interfaces;
@@ -7,7 +9,8 @@ namespace Umbraco.Tests.Common.Builders
{
public class AuditEntryBuilder : AuditEntryBuilder