diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 1d136daf95..92cb0f065e 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -43,18 +43,15 @@ - - - diff --git a/build/build.ps1 b/build/build.ps1 index 07d856d075..58d56fcdfe 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -478,7 +478,7 @@ { $this.VerifyNuGetConsistency( ("UmbracoCms", "UmbracoCms.Core", "UmbracoCms.Web"), - ("Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.Web.UI.NetCore", "Umbraco.Examine.Lucene", "Umbraco.PublishedCache.NuCache", "Umbraco.Web.Common", "Umbraco.Web.Website", "Umbraco.Web.BackOffice", "Umbraco.ModelsBuilder.Embedded", "Umbraco.Persistence.SqlCe")) + ("Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.Web.UI.NetCore", "Umbraco.Examine.Lucene", "Umbraco.PublishedCache.NuCache", "Umbraco.Web.Common", "Umbraco.Web.Website", "Umbraco.Web.BackOffice", "Umbraco.Persistence.SqlCe")) if ($this.OnError()) { return } }) diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index bd0deaf6bb..516f26774a 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; using Microsoft.Extensions.Logging; @@ -21,7 +21,6 @@ namespace Umbraco.Cms.Core.Composing "Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", - "Umbraco.ModelsBuilder.Embedded", "Umbraco.Examine.Lucene", "Umbraco.Web.Common", "Umbraco.Web.BackOffice", diff --git a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs index 8bc98f4286..c7ce20454f 100644 --- a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs @@ -1,7 +1,7 @@ namespace Umbraco.Cms.Core.Configuration { /// - /// The password configuration for back office users + /// The password configuration for members /// public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration { diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 51b7a1824c..ba0f1e0a37 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -41,6 +41,9 @@ namespace Umbraco.Cms.Core public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__"; + public const string DefaultMemberTypeAlias = "Member"; + + /// /// The prefix used for external identity providers for their authentication type /// diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs b/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs similarity index 83% rename from src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs rename to src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs index 56ee0b983d..9ba5c9dd0c 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs @@ -1,8 +1,7 @@ using System; using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Dashboards; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Core.Dashboards { [Weight(40)] public class ModelsBuilderDashboard : IDashboard diff --git a/src/Umbraco.Core/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs index 9a7016b6be..258c4a7f64 100644 --- a/src/Umbraco.Core/IO/ViewHelper.cs +++ b/src/Umbraco.Core/IO/ViewHelper.cs @@ -71,7 +71,7 @@ namespace Umbraco.Cms.Core.IO // @inherits Umbraco.Web.Mvc.UmbracoViewPage // @inherits Umbraco.Web.Mvc.UmbracoViewPage content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;"); - content.Append("@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage"); + content.Append("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"); if (modelClassName.IsNullOrWhiteSpace() == false) { content.Append("<"); diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs new file mode 100644 index 0000000000..d4cbe6c95f --- /dev/null +++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs @@ -0,0 +1,33 @@ +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Core.Models.Mapping +{ + /// + public class MemberMapDefinition : IMapDefinition + { + /// + public void DefineMaps(UmbracoMapper mapper) => mapper.Define(Map); + + private static void Map(MemberSave source, IMember target, MapperContext context) + { + target.IsApproved = source.IsApproved; + target.Name = source.Name; + target.Email = source.Email; + target.Key = source.Key; + target.Username = source.Username; + target.Comments = source.Comments; + target.CreateDate = source.CreateDate; + target.UpdateDate = source.UpdateDate; + target.Email = source.Email; + + // TODO: ensure all properties are mapped as required + //target.Id = source.Id; + //target.ParentId = -1; + //target.Path = "-1," + source.Id; + + //TODO: add groups as required + } + } +} diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index e54203619e..92aab36bd4 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Options; diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 455aa44c13..e218113bf6 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; @@ -19,8 +19,11 @@ namespace Umbraco.Cms.Core.Models private string _email; private string _rawPasswordValue; private string _passwordConfig; + private DateTime? _emailConfirmedDate; + private string _securityStamp; /// + /// Initializes a new instance of the class. /// Constructor for creating an empty Member object /// /// ContentType for the current Content object @@ -29,13 +32,14 @@ namespace Umbraco.Cms.Core.Models { IsApproved = true; - //this cannot be null but can be empty + // this cannot be null but can be empty _rawPasswordValue = ""; _email = ""; _username = ""; } /// + /// Initializes a new instance of the class. /// Constructor for creating a Member object /// /// Name of the content @@ -43,18 +47,21 @@ namespace Umbraco.Cms.Core.Models public Member(string name, IMemberType contentType) : base(name, -1, contentType, new PropertyCollection()) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); IsApproved = true; - //this cannot be null but can be empty + // this cannot be null but can be empty _rawPasswordValue = ""; _email = ""; _username = ""; } /// + /// Initializes a new instance of the class. /// Constructor for creating a Member object /// /// @@ -64,22 +71,29 @@ namespace Umbraco.Cms.Core.Models public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true) : base(name, -1, contentType, new PropertyCollection()) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - if (email == null) throw new ArgumentNullException(nameof(email)); - if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email)); - if (username == null) throw new ArgumentNullException(nameof(username)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + if (email == null) + throw new ArgumentNullException(nameof(email)); + if (string.IsNullOrWhiteSpace(email)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email)); + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (string.IsNullOrWhiteSpace(username)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); _email = email; _username = username; IsApproved = isApproved; - //this cannot be null but can be empty + // this cannot be null but can be empty _rawPasswordValue = ""; } /// + /// Initializes a new instance of the class. /// Constructor for creating a Member object /// /// @@ -99,6 +113,7 @@ namespace Umbraco.Cms.Core.Models } /// + /// Initializes a new instance of the class. /// Constructor for creating a Member object /// /// @@ -138,6 +153,13 @@ namespace Umbraco.Cms.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email)); } + [DataMember] + public DateTime? EmailConfirmedDate + { + get => _emailConfirmedDate; + set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + } + /// /// Gets or sets the raw password value /// @@ -190,7 +212,8 @@ namespace Umbraco.Cms.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string)); - if (a.Success == false) return a.Result; + if (a.Success == false) + return a.Result; return Properties[Constants.Conventions.Member.Comments].GetValue() == null ? string.Empty @@ -200,7 +223,8 @@ namespace Umbraco.Cms.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.Comments, - nameof(Comments)) == false) return; + nameof(Comments)) == false) + return; Properties[Constants.Conventions.Member.Comments].SetValue(value); } @@ -221,8 +245,10 @@ namespace Umbraco.Cms.Core.Models var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsApproved, nameof(IsApproved), //This is the default value if the prop is not found true); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.IsApproved].GetValue() == null) return true; + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.IsApproved].GetValue() == null) + return true; var tryConvert = Properties[Constants.Conventions.Member.IsApproved].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -235,7 +261,8 @@ namespace Umbraco.Cms.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.IsApproved, - nameof(IsApproved)) == false) return; + nameof(IsApproved)) == false) + return; Properties[Constants.Conventions.Member.IsApproved].SetValue(value); } @@ -254,8 +281,10 @@ namespace Umbraco.Cms.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsLockedOut, nameof(IsLockedOut), false); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.IsLockedOut].GetValue() == null) return false; + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.IsLockedOut].GetValue() == null) + return false; var tryConvert = Properties[Constants.Conventions.Member.IsLockedOut].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -268,7 +297,8 @@ namespace Umbraco.Cms.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.IsLockedOut, - nameof(IsLockedOut)) == false) return; + nameof(IsLockedOut)) == false) + return; Properties[Constants.Conventions.Member.IsLockedOut].SetValue(value); } @@ -287,8 +317,10 @@ namespace Umbraco.Cms.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLoginDate, nameof(LastLoginDate), default(DateTime)); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.LastLoginDate].GetValue() == null) return default(DateTime); + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.LastLoginDate].GetValue() == null) + return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastLoginDate].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -301,7 +333,8 @@ namespace Umbraco.Cms.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastLoginDate, - nameof(LastLoginDate)) == false) return; + nameof(LastLoginDate)) == false) + return; Properties[Constants.Conventions.Member.LastLoginDate].SetValue(value); } @@ -320,8 +353,10 @@ namespace Umbraco.Cms.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastPasswordChangeDate, nameof(LastPasswordChangeDate), default(DateTime)); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue() == null) return default(DateTime); + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue() == null) + return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -334,7 +369,8 @@ namespace Umbraco.Cms.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastPasswordChangeDate, - nameof(LastPasswordChangeDate)) == false) return; + nameof(LastPasswordChangeDate)) == false) + return; Properties[Constants.Conventions.Member.LastPasswordChangeDate].SetValue(value); } @@ -353,8 +389,10 @@ namespace Umbraco.Cms.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLockoutDate, nameof(LastLockoutDate), default(DateTime)); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.LastLockoutDate].GetValue() == null) return default(DateTime); + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.LastLockoutDate].GetValue() == null) + return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastLockoutDate].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -367,7 +405,8 @@ namespace Umbraco.Cms.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastLockoutDate, - nameof(LastLockoutDate)) == false) return; + nameof(LastLockoutDate)) == false) + return; Properties[Constants.Conventions.Member.LastLockoutDate].SetValue(value); } @@ -387,8 +426,10 @@ namespace Umbraco.Cms.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.FailedPasswordAttempts, nameof(FailedPasswordAttempts), 0); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue() == null) return default(int); + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue() == null) + return default(int); var tryConvert = Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -401,7 +442,8 @@ namespace Umbraco.Cms.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.FailedPasswordAttempts, - nameof(FailedPasswordAttempts)) == false) return; + nameof(FailedPasswordAttempts)) == false) + return; Properties[Constants.Conventions.Member.FailedPasswordAttempts].SetValue(value); } @@ -413,6 +455,17 @@ namespace Umbraco.Cms.Core.Models [DataMember] public virtual string ContentTypeAlias => ContentType.Alias; + /// + /// The security stamp used by ASP.Net identity + /// + [IgnoreDataMember] + public string SecurityStamp + { + get => _securityStamp; + set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); + } + + /// /// Internal/Experimental - only used for mapping queries. /// diff --git a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs index 3374f1f11a..29a6bf4cdb 100644 --- a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs +++ b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models.Membership @@ -10,6 +10,7 @@ namespace Umbraco.Cms.Core.Models.Membership { string Username { get; set; } string Email { get; set; } + DateTime? EmailConfirmedDate { get; set; } /// /// Gets or sets the raw password value @@ -38,6 +39,11 @@ namespace Umbraco.Cms.Core.Models.Membership /// int FailedPasswordAttempts { get; set; } + /// + /// Gets or sets the security stamp used by ASP.NET Identity + /// + string SecurityStamp { get; set; } + //object ProfileId { get; set; } //IEnumerable Groups { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index f9763bc7e9..a36b155da5 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; @@ -19,7 +19,6 @@ namespace Umbraco.Cms.Core.Models.Membership int[] StartMediaIds { get; set; } string Language { get; set; } - DateTime? EmailConfirmedDate { get; set; } DateTime? InvitedDate { get; set; } /// @@ -38,11 +37,6 @@ namespace Umbraco.Cms.Core.Models.Membership /// IProfile ProfileData { get; } - /// - /// The security stamp used by ASP.Net identity - /// - string SecurityStamp { get; set; } - /// /// Will hold the media file system relative path of the users custom avatar if they uploaded one /// diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index ede9e49a7d..9f49dade80 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -8,7 +8,6 @@ using System.Runtime.InteropServices; // Umbraco Cms [assembly: InternalsVisibleTo("Umbraco.Web")] [assembly: InternalsVisibleTo("Umbraco.Web.UI")] -[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs index e84a312a8f..b9e5912c1b 100644 --- a/src/Umbraco.Core/Services/IMembershipRoleService.cs +++ b/src/Umbraco.Core/Services/IMembershipRoleService.cs @@ -1,29 +1,47 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; namespace Umbraco.Cms.Core.Services { - public interface IMembershipRoleService + public interface IMembershipRoleService where T : class, IMembershipUser { void AddRole(string roleName); - IEnumerable GetAllRoles(); + + IEnumerable GetAllRoles(); + IEnumerable GetAllRoles(int memberId); + IEnumerable GetAllRoles(string username); + IEnumerable GetAllRolesIds(); + IEnumerable GetAllRolesIds(int memberId); + IEnumerable GetAllRolesIds(string username); + IEnumerable GetMembersInRole(string roleName); + IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + bool DeleteRole(string roleName, bool throwIfBeingUsed); + void AssignRole(string username, string roleName); + void AssignRoles(string[] usernames, string[] roleNames); + void DissociateRole(string username, string roleName); + void DissociateRoles(string[] usernames, string[] roleNames); + void AssignRole(int memberId, string roleName); + void AssignRoles(int[] memberIds, string[] roleNames); + void DissociateRole(int memberId, string roleName); + void DissociateRoles(int[] memberIds, string[] roleNames); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 933c00460c..2f38e4953b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 56c7d20f85..269a74fcda 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; diff --git a/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs b/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs index ff7637eff0..f76d3b3a91 100644 --- a/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Text; using System.Threading; diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs index 0f0ecc21f9..ca51c5df54 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.Mapping; +using Umbraco.Core.Models.Mapping; using Umbraco.Core.Security; using Umbraco.Extensions; using Umbraco.Web.Models.Mapping; @@ -32,6 +33,7 @@ namespace Umbraco.Infrastructure.DependencyInjection .Add() .Add() .Add() + .Add() .Add() .Add(); diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index 22dd3d4276..589a70b7b7 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -7,12 +7,12 @@ using System.IO; using System.Linq; using System.Security.AccessControl; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.IO; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Infrastructure.Install { @@ -27,6 +27,7 @@ namespace Umbraco.Infrastructure.Install private readonly string[] _permissionFiles = Array.Empty(); private readonly GlobalSettings _globalSettings; private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; private string _basePath; /// @@ -36,6 +37,7 @@ namespace Umbraco.Infrastructure.Install { _globalSettings = globalSettings.Value; _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; _basePath = hostingEnvironment.MapPathContentRoot("/"); _permissionDirs = new[] { @@ -68,7 +70,7 @@ namespace Umbraco.Infrastructure.Install EnsureFiles(_permissionFiles, out errors); report[FilePermissionTest.FileWriting] = errors.ToList(); - EnsureCanCreateSubDirectory(_globalSettings.UmbracoMediaPath, out errors); + EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), out errors); report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); return report.Sum(x => x.Value.Count()) == 0; diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index e4db6cf609..282886a6ea 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using System.Net.Http; using System.Text; diff --git a/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs similarity index 81% rename from src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs index 14f4478d24..efd7257414 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Reflection; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { /// /// Manages API version handshake between client and server. @@ -14,10 +14,7 @@ namespace Umbraco.Cms.ModelsBuilder.Embedded /// /// The currently executing version. /// - internal ApiVersion(SemVersion executingVersion) - { - Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); - } + internal ApiVersion(SemVersion executingVersion) => Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); private static SemVersion CurrentAssemblyVersion => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs index 2a59aa01a1..27bd6d990a 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.ModelsBuilder.Embedded.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { // NOTE // The idea was to have different types of builder, because I wanted to experiment with @@ -31,7 +31,7 @@ namespace Umbraco.Cms.ModelsBuilder.Embedded.Building "Umbraco.Cms.Core.Models.PublishedContent", "Umbraco.Web.PublishedCache", // Todo: Remove/Edit this once namespaces has been aligned. "Umbraco.Cms.Core.PublishedCache", - "Umbraco.Cms.ModelsBuilder.Embedded", + "Umbraco.Cms.Infrastructure.ModelsBuilder", "Umbraco.Cms.Core", "Umbraco.Core", // TODO: Remove once namespace is gone, after which BuilderTests needs to be adjusted. "Umbraco.Extensions" diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs similarity index 96% rename from src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs index 5777dc1f7d..ac4780cb7c 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { public class ModelsGenerator { @@ -22,7 +22,7 @@ namespace Umbraco.Cms.ModelsBuilder.Embedded.Building _hostingEnvironment = hostingEnvironment; } - internal void GenerateModels() + public void GenerateModels() { var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs similarity index 96% rename from src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs index c07affd61d..fbb29e3ebd 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs @@ -1,7 +1,7 @@ -using System; +using System; using System.Collections.Generic; -namespace Umbraco.Cms.ModelsBuilder.Embedded.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { /// /// Represents a model property. diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs similarity index 99% rename from src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs index 077a84bcfa..7acfbcf26a 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs @@ -1,16 +1,16 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.ModelsBuilder.Embedded.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { /// /// Implements a builder that works by writing text. /// - internal class TextBuilder : Builder + public class TextBuilder : Builder { /// /// Initializes a new instance of the class with a list of models to generate diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs similarity index 92% rename from src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs index 6579e2a541..a192560f1d 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs @@ -1,6 +1,6 @@ -using System.Text; +using System.Text; -namespace Umbraco.Cms.ModelsBuilder.Embedded.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { internal static class TextHeaderWriter { diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs index 82216fa49e..e5ee0a36b5 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.ModelsBuilder.Embedded.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { /// /// Represents a model. diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs similarity index 93% rename from src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs index d4cd4bf8ec..46af457299 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { - internal class TypeModelHasher + public class TypeModelHasher { public static string Hash(IEnumerable typeModels) { diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs new file mode 100644 index 0000000000..474bea9251 --- /dev/null +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace Umbraco.Cms.Infrastructure.ModelsBuilder +{ + /// + /// Indicates that a property implements a given property alias. + /// + /// And therefore it should not be generated. + [AttributeUsage(AttributeTargets.Property /*, AllowMultiple = false, Inherited = false*/)] + public class ImplementPropertyTypeAttribute : Attribute + { + public ImplementPropertyTypeAttribute(string alias) => Alias = alias; + + public string Alias { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.Infrastructure/ModelsBuilder/LiveModelsProvider.cs similarity index 97% rename from src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/LiveModelsProvider.cs index eb8249c0ba..2c0a71016a 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/LiveModelsProvider.cs @@ -6,10 +6,10 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Runtime; -using Umbraco.Cms.ModelsBuilder.Embedded.Building; +using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { // supports LiveAppData - but not PureLive public sealed class LiveModelsProvider : INotificationHandler, INotificationHandler diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs similarity index 92% rename from src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs index 1bf85ff38f..6754060d6f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { /// /// Indicates that an Assembly is a Models Builder assembly. diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs similarity index 97% rename from src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs index 47e4579a59..18b28c2bba 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { public sealed class ModelsGenerationError { diff --git a/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs index dcddd9ff80..65a7ac3ef8 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { /// /// Used to track if ModelsBuilder models are out of date/stale diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs similarity index 95% rename from src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs index 29ca0c86e2..88e873517b 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs @@ -1,13 +1,12 @@ -using System; +using System; using System.Linq.Expressions; using System.Reflection; using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.ModelsBuilder.Embedded; -using Umbraco.Extensions; +using Umbraco.Cms.Infrastructure.ModelsBuilder; // same namespace as original Umbraco.Web PublishedElementExtensions // ReSharper disable once CheckNamespace -namespace Umbraco.Core +namespace Umbraco.Extensions { /// /// Provides extension methods to models. diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs index e990ea5030..82cf52a131 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Linq; using System.Linq.Expressions; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { /// /// This is called from within the generated model classes diff --git a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs index 2009e74638..15d2114f4d 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { public class RoslynCompiler { diff --git a/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs similarity index 93% rename from src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs index 29e929134e..e9f427b158 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { internal static class TypeExtensions { diff --git a/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs index c4a68c66c3..b4963639c4 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs @@ -6,10 +6,10 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.ModelsBuilder.Embedded.Building; +using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Infrastructure.ModelsBuilder { public sealed class UmbracoServices diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 978e04bc6d..1fd2659775 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -353,7 +353,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // if parent has changed, get path, level and sort order if (entity.IsPropertyDirty("ParentId")) { - var parent = GetParentNodeDto(entity.ParentId); + NodeDto parent = GetParentNodeDto(entity.ParentId); entity.Path = string.Concat(parent.Path, ",", entity.Id); entity.Level = parent.Level + 1; @@ -361,10 +361,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // create the dto - var dto = ContentBaseFactory.BuildDto(entity); + MemberDto dto = ContentBaseFactory.BuildDto(entity); // update the node dto - var nodeDto = dto.ContentDto.NodeDto; + NodeDto nodeDto = dto.ContentDto.NodeDto; Database.Update(nodeDto); // update the content dto @@ -415,7 +415,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //get the group id var grpQry = Query().Where(group => group.Name.Equals(roleName)); var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); - if (memberGroup == null) return Enumerable.Empty(); + if (memberGroup == null) + return Enumerable.Empty(); // get the members by username var query = Query(); @@ -470,7 +471,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var grpQry = Query().Where(group => group.Name.Equals(groupName)); var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); - if (memberGroup == null) return Enumerable.Empty(); + if (memberGroup == null) + return Enumerable.Empty(); var subQuery = Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); @@ -620,7 +622,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { - content[i] = (Member) cached; + content[i] = (Member)cached; continue; } } @@ -662,7 +664,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get properties - indexed by version id var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.ContentDto.NodeId,versionId, 0, memberType); + var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); var properties = GetPropertyCollections(new List> { temp }); member.Properties = properties[versionId]; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs b/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs index b688108d24..9e306f3ade 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Composing; diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index 8be81b44d4..ee170afa77 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 33012c21c4..0d86845f6b 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -94,7 +94,9 @@ namespace Umbraco.Core.Security set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); } - + /// + /// Gets or sets the password config + /// public string PasswordConfig { get => _passwordConfig; @@ -187,13 +189,13 @@ namespace Umbraco.Core.Security { get { - var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; + bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; return isLocked; } } /// - /// Gets or sets a value indicating the IUser IsApproved + /// Gets or sets a value indicating whether the IUser IsApproved /// public bool IsApproved { get; set; } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index ad95a5df64..f5ca6318c7 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -439,7 +439,7 @@ namespace Umbraco.Core.Security } /// - /// Returns the roles (user groups) for this user + /// Gets a list of role names the specified user belongs to. /// public override Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs index 8b2c8932a7..cb6caa3345 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Umbraco.Core.Security; namespace Umbraco.Core.Security { diff --git a/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs index 4235195bb1..42a31787d9 100644 --- a/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs @@ -1,5 +1,3 @@ -using Umbraco.Core.Security; - namespace Umbraco.Core.Security { /// diff --git a/src/Umbraco.Infrastructure/Security/IMemberManager.cs b/src/Umbraco.Infrastructure/Security/IMemberManager.cs new file mode 100644 index 0000000000..873e0eb0b4 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/IMemberManager.cs @@ -0,0 +1,11 @@ +using Umbraco.Core.Security; + +namespace Umbraco.Core.Security +{ + /// + /// The user manager for members + /// + public interface IMemberManager : IUmbracoUserManager + { + } +} diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index 5669717aec..bbfcc97522 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Core.Security; +using Umbraco.Core.Models.Identity; namespace Umbraco.Core.Security { @@ -16,7 +16,7 @@ namespace Umbraco.Core.Security /// /// The type of user public interface IUmbracoUserManager : IDisposable - where TUser : BackOfficeIdentityUser + where TUser : UmbracoIdentityUser { /// /// Gets the user id of a user @@ -223,12 +223,64 @@ namespace Umbraco.Core.Security /// Task CreateAsync(TUser user); + /// + /// Gets a list of role names the specified user belongs to. + /// + /// The user whose role names to retrieve. + /// The Task that represents the asynchronous operation, containing a list of role names. + Task> GetRolesAsync(TUser user); + + /// + /// Removes the specified user from the named roles. + /// + /// The user to remove from the named roles. + /// The name of the roles to remove the user from. + /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation. + Task RemoveFromRolesAsync(TUser user, IEnumerable roles); + + /// + /// Add the specified user to the named roles + /// + /// The user to add to the named roles + /// The name of the roles to add the user to. + /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation + Task AddToRolesAsync(TUser user, IEnumerable roles); + + /// + /// Creates the specified in the backing store with a password, + /// as an asynchronous operation. + /// + /// The user to create. + /// The password to add to the user. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task CreateAsync(TUser user, string password); + /// /// Generate a password for a user based on the current password validator /// /// A generated password string GeneratePassword(); + /// + /// Hashes a password for a null user based on the default password hasher + /// + /// The password to hash + /// The hashed password + string HashPassword(string password); + + /// + /// Used to validate the password without an identity user + /// Validation code is based on the default ValidatePasswordAsync code + /// Should return if validation is successful + /// + /// The password. + /// A representing whether validation was successful. + + Task ValidatePasswordAsync(string password); + /// /// Generates an email confirmation token for the specified user. /// diff --git a/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs index 95a63c6001..d7ec33d1f3 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Identity; -namespace Umbraco.Extensions +namespace Umbraco.Core.Security { public static class IdentityExtensions { diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 8d57fc92be..ccbb1b0a18 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -3,6 +3,7 @@ using System; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -40,6 +41,20 @@ namespace Umbraco.Core.Security target.ResetDirtyProperties(true); target.EnableChangeTracking(); }); + + mapper.Define( + (source, context) => + { + var target = new MembersIdentityUser(source.Id); + target.DisableChangeTracking(); + return target; + }, + (source, target, context) => + { + Map(source, target); + target.ResetDirtyProperties(true); + target.EnableChangeTracking(); + }); } // Umbraco.Code.MapAll -Id -Groups -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled @@ -79,9 +94,24 @@ namespace Umbraco.Core.Security //target.Roles =; } - private static string GetPasswordHash(string storedPass) + private void Map(IMember source, MembersIdentityUser target) { - return storedPass.StartsWith(Cms.Core.Constants.Security.EmptyPasswordPrefix) ? null : storedPass; + target.Email = source.Email; + target.UserName = source.Username; + target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); + target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); + target.EmailConfirmed = source.EmailConfirmedDate.HasValue; + target.Name = source.Name; + target.AccessFailedCount = source.FailedPasswordAttempts; + target.PasswordHash = GetPasswordHash(source.RawPasswordValue); + target.PasswordConfig = source.PasswordConfiguration; + target.IsApproved = source.IsApproved; + target.SecurityStamp = source.SecurityStamp; + target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; + + // NB: same comments re AutoMapper as per BackOfficeUser } + + private static string GetPasswordHash(string storedPass) => storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass; } } diff --git a/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs new file mode 100644 index 0000000000..6026efa62f --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core.Services; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Security +{ + /// + /// A custom user store that uses Umbraco member data + /// + public class MemberRolesUserStore : RoleStoreBase, string, IdentityUserRole, IdentityRoleClaim> + { + private readonly IMemberService _memberService; + private readonly IMemberGroupService _memberGroupService; + private readonly IScopeProvider _scopeProvider; + + public MemberRolesUserStore(IMemberService memberService, IMemberGroupService memberGroupService, IScopeProvider scopeProvider, IdentityErrorDescriber describer) + : base(describer) + { + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); + _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); + } + + /// + public override IQueryable> Roles { get; } + + /// + public override Task CreateAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task> FindByIdAsync(string id, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task> GetClaimsAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task AddClaimAsync(IdentityRole role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task RemoveClaimAsync(IdentityRole role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + } +} diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs new file mode 100644 index 0000000000..30229e4521 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; + +namespace Umbraco.Core.Security +{ + public class MembersIdentityBuilder : IdentityBuilder + { + public MembersIdentityBuilder(IServiceCollection services) : base(typeof(MembersIdentityUser), services) + { + } + + public MembersIdentityBuilder(Type role, IServiceCollection services) : base(typeof(MembersIdentityUser), role, services) + { + } + + /// + /// Adds a token provider for the . + /// + /// The name of the provider to add. + /// The type of the to add. + /// The current instance. + public override IdentityBuilder AddTokenProvider(string providerName, Type provider) + { + if (!typeof(IUserTwoFactorTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo())) + { + throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}"); + } + Services.Configure(options => + { + options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider); + }); + Services.AddTransient(provider); + return this; + } + } +} diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs new file mode 100644 index 0000000000..d8db1d29a0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// Identity options specifically for the Umbraco members identity implementation + /// + public class MembersIdentityOptions : IdentityOptions + { + } +} diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs new file mode 100644 index 0000000000..57d5365b33 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Core; +using Umbraco.Core.Models.Identity; +using Umbraco.Extensions; + +namespace Umbraco.Core.Security +{ + /// + /// The identity user used for the member + /// + public class MembersIdentityUser : UmbracoIdentityUser + { + private string _name; + private string _passwordConfig; + private IReadOnlyCollection _groups; + + // 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()); + + /// + /// Initializes a new instance of the class. + /// + public MembersIdentityUser(int userId) + { + // use the property setters - they do more than just setting a field + Id = UserIdToString(userId); + } + + public MembersIdentityUser() + { + } + + /// + /// Used to construct a new instance without an identity + /// + public static MembersIdentityUser CreateNew(string username, string email, string memberTypeAlias, string name = null) + { + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + } + + var user = new MembersIdentityUser(); + user.DisableChangeTracking(); + user.UserName = username; + user.Email = email; + user.MemberTypeAlias = memberTypeAlias; + user.Id = null; + user.HasIdentity = false; + user._name = name; + user.EnableChangeTracking(); + return user; + } + + /// + /// Gets or sets the member's real name + /// + public string Name + { + get => _name; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets the password config + /// + public string PasswordConfig + { + get => _passwordConfig; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); + } + + /// + /// Gets or sets the user groups + /// + public IReadOnlyCollection Groups + { + get => _groups; + set + { + _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 + })) + { + roles.Add(identityUserRole); + } + + // now reset the collection + Roles = roles; + + BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer); + } + } + + /// + /// Gets a value indicating whether the member is locked out + /// + public bool IsLockedOut + { + get + { + bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; + return isLocked; + } + } + + /// + /// Gets or sets a value indicating whether the member is approved + /// + public bool IsApproved { get; set; } + + /// + /// Gets or sets the alias of the member type + /// + public string MemberTypeAlias { get; set; } + + private static string UserIdToString(int userId) => string.Intern(userId.ToString()); + } +} diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs new file mode 100644 index 0000000000..332308bbc7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -0,0 +1,702 @@ +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 Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Services; +using Umbraco.Core.Scoping; +using Umbraco.Core.Security; +using Umbraco.Extensions; + +namespace Umbraco.Core.Security +{ + /// + /// A custom user store that uses Umbraco member data + /// + public class MembersUserStore : UserStoreBase, string, IdentityUserClaim, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + { + private readonly IMemberService _memberService; + private readonly UmbracoMapper _mapper; + private readonly IScopeProvider _scopeProvider; + + /// + /// Initializes a new instance of the class for the members identity store + /// + /// The member service + /// The mapper for properties + /// The scope provider + /// The error describer + public MembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer) + : base(describer) + { + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); + } + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override IQueryable Users => throw new NotImplementedException(); + + /// + public override Task GetNormalizedUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken); + + /// + public override Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); + + /// + public override Task CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + // create member + IMember memberEntity = _memberService.CreateMember( + user.UserName, + user.Email, + user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, + user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); + + UpdateMemberProperties(memberEntity, user); + + // create the member + _memberService.Save(memberEntity); + + if (!memberEntity.HasIdentity) + { + throw new DataException("Could not create the member, check logs for details"); + } + + // re-assign id + user.Id = UserIdToString(memberEntity.Id); + + // [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + // var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); + // TODO: confirm re externallogins implementation + //if (isLoginsPropertyDirty) + //{ + // _externalLoginService.Save( + // user.Id, + // user.Logins.Select(x => new ExternalLogin( + // x.LoginProvider, + // x.ProviderKey, + // x.UserData))); + //} + + return Task.FromResult(IdentityResult.Success); + } + + /// + public override Task UpdateAsync(MembersIdentityUser 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()) + { + IMember found = _memberService.GetById(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(MembersIdentityUser.Logins)); + + if (UpdateMemberProperties(found, user)) + { + _memberService.Save(found); + } + + // TODO: when to implement external login service? + + //if (isLoginsPropertyDirty) + //{ + // _externalLoginService.Save( + // found.Id, + // user.Logins.Select(x => new ExternalLogin( + // x.LoginProvider, + // x.ProviderKey, + // x.UserData))); + //} + } + + scope.Complete(); + } + + return Task.FromResult(IdentityResult.Success); + } + + /// + public override Task DeleteAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IMember found = _memberService.GetById(UserIdToInt(user.Id)); + if (found != null) + { + _memberService.Delete(found); + } + + // TODO: when to implement external login service? + //_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(); + + IMember user = _memberService.GetById(UserIdToInt(userId)); + if (user == null) + { + return Task.FromResult((MembersIdentityUser)null); + } + + return Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); + } + + /// + public override Task FindByNameAsync(string userName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + IMember user = _memberService.GetByUsername(userName); + if (user == null) + { + return Task.FromResult((MembersIdentityUser)null); + } + + MembersIdentityUser result = AssignLoginsCallback(_mapper.Map(user)); + + return Task.FromResult(result); + } + + /// + public override async Task SetPasswordHashAsync(MembersIdentityUser 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(MembersIdentityUser 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 false; + } + + /// + public override Task FindByEmailAsync(string email, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + IMember member = _memberService.GetByEmail(email); + MembersIdentityUser result = member == null + ? null + : _mapper.Map(member); + + return Task.FromResult(AssignLoginsCallback(result)); + } + + /// + public override Task GetNormalizedEmailAsync(MembersIdentityUser user, CancellationToken cancellationToken) + => GetEmailAsync(user, cancellationToken); + + /// + public override Task SetNormalizedEmailAsync(MembersIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + => SetEmailAsync(user, normalizedEmail, cancellationToken); + + /// + public override Task AddLoginAsync(MembersIdentityUser 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(MembersIdentityUser 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(MembersIdentityUser 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(); + + MembersIdentityUser 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 = new List(); + + // TODO: external login needed? + //_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 + }); + } + + /// + public override Task AddToRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + if (string.IsNullOrWhiteSpace(role)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); + + if (userRole == null) + { + _memberService.AssignRole(user.UserName, role); + user.AddRole(role); + } + + return Task.CompletedTask; + } + + /// + public override Task RemoveFromRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + if (string.IsNullOrWhiteSpace(role)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); + + if (userRole != null) + { + _memberService.DissociateRole(user.UserName, userRole.RoleId); + user.Roles.Remove(userRole); + } + + return Task.CompletedTask; + } + + /// + /// Gets a list of role names the specified user belongs to. + /// + public override Task> GetRolesAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IEnumerable currentRoles = _memberService.GetAllRoles(user.UserName); + ICollection> roles = currentRoles.Select(role => new IdentityUserRole + { + RoleId = role, + UserId = user.Id + }).ToList(); + + user.Roles = roles; + return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); + } + + /// + /// Returns true if a user is in the role + /// + public override Task IsInRoleAsync(MembersIdentityUser 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. + /// + public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (normalizedRoleName == null) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } + + IEnumerable members = _memberService.GetMembersByMemberType(normalizedRoleName); + + IList membersIdentityUsers = members.Select(x => _mapper.Map(x)).ToList(); + + return Task.FromResult(membersIdentityUsers); + } + + /// + protected override Task> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == normalizedRoleName); + if (group == null) + { + return Task.FromResult((IdentityRole)null); + } + + return Task.FromResult(new IdentityRole(group.Name) + { + //TODO: what should the alias be? + Id = @group.Id.ToString() + }); + } + + /// + protected override async Task> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken) + { + MembersIdentityUser 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(MembersIdentityUser 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 MembersIdentityUser AssignLoginsCallback(MembersIdentityUser user) + { + if (user != null) + { + //TODO: when to + //user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id)))); + } + + return user; + } + + private bool UpdateMemberProperties(IMember member, MembersIdentityUser identityUserMember) + { + var anythingChanged = false; + + // don't assign anything if nothing has changed as this will trigger the track changes of the model + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastLoginDateUtc)) + || (member.LastLoginDate != default && identityUserMember.LastLoginDateUtc.HasValue == false) + || (identityUserMember.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUserMember.LastLoginDateUtc.Value)) + { + anythingChanged = true; + + // if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime + DateTime dt = identityUserMember.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUserMember.LastLoginDateUtc.Value.ToLocalTime(); + member.LastLoginDate = dt; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastPasswordChangeDateUtc)) + || (member.LastPasswordChangeDate != default && identityUserMember.LastPasswordChangeDateUtc.HasValue == false) + || (identityUserMember.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUserMember.LastPasswordChangeDateUtc.Value)) + { + anythingChanged = true; + member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime(); + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.EmailConfirmed)) + || (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUserMember.EmailConfirmed == false) + || ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUserMember.EmailConfirmed)) + { + anythingChanged = true; + member.EmailConfirmedDate = identityUserMember.EmailConfirmed ? (DateTime?)DateTime.Now : null; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Name)) + && member.Name != identityUserMember.Name && identityUserMember.Name.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Name = identityUserMember.Name; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Email)) + && member.Email != identityUserMember.Email && identityUserMember.Email.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Email = identityUserMember.Email; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.AccessFailedCount)) + && member.FailedPasswordAttempts != identityUserMember.AccessFailedCount) + { + anythingChanged = true; + member.FailedPasswordAttempts = identityUserMember.AccessFailedCount; + } + + if (member.IsLockedOut != identityUserMember.IsLockedOut) + { + anythingChanged = true; + member.IsLockedOut = identityUserMember.IsLockedOut; + + if (member.IsLockedOut) + { + // need to set the last lockout date + member.LastLockoutDate = DateTime.Now; + } + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.UserName)) + && member.Username != identityUserMember.UserName && identityUserMember.UserName.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Username = identityUserMember.UserName; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.PasswordHash)) + && member.RawPasswordValue != identityUserMember.PasswordHash && identityUserMember.PasswordHash.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.RawPasswordValue = identityUserMember.PasswordHash; + member.PasswordConfiguration = identityUserMember.PasswordConfig; + } + + if (member.SecurityStamp != identityUserMember.SecurityStamp) + { + anythingChanged = true; + member.SecurityStamp = identityUserMember.SecurityStamp; + } + + // TODO: Fix this for Groups too (as per backoffice comment) + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Groups))) + { + } + + // reset all changes + identityUserMember.ResetDirtyProperties(false); + + return anythingChanged; + } + + 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(MembersIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task AddClaimsAsync(MembersIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task ReplaceClaimAsync(MembersIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task RemoveClaimsAsync(MembersIdentityUser 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(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override Task> FindTokenAsync(MembersIdentityUser 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/Security/BackOfficeLookupNormalizer.cs b/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs similarity index 76% rename from src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs rename to src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs index 957e36d1d0..5952f1bed5 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs +++ b/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs @@ -6,10 +6,8 @@ namespace Umbraco.Core.Security /// /// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2 /// - public class BackOfficeLookupNormalizer : ILookupNormalizer + public class NoopLookupNormalizer : 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/SignOutAuditEventArgs.cs b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs index b3eb56ce01..7321b7bd7b 100644 --- a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs @@ -1,9 +1,7 @@ using Umbraco.Cms.Core.Security; -using Umbraco.Core.Security; namespace Umbraco.Core.Security { - /// /// Event args used when signing out /// diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 8c295f5a0f..fc0744da4e 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; @@ -10,10 +11,10 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; namespace Umbraco.Core.Security { - /// /// Abstract class for Umbraco User Managers for back office users or front-end members /// @@ -35,12 +36,11 @@ namespace Umbraco.Core.Security 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) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, new NoopLookupNormalizer(), errors, services, logger) { IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); @@ -51,7 +51,7 @@ namespace Umbraco.Core.Security /// 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 /// @@ -75,8 +75,8 @@ namespace Umbraco.Core.Security /// Used to validate a user's session /// /// The user id - /// The sesion id - /// True if the sesion is valid, else false + /// The session id + /// True if the session is valid, else false public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) { var userSessionStore = Store as IUserSessionStore; @@ -91,26 +91,62 @@ namespace Umbraco.Core.Security 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); + + string password = _passwordGenerator.GeneratePassword(); + return password; + } + + /// + /// Generates a hashed password based on the default password hasher + /// No existing identity user is required and this does not validate the password + /// + /// The password to hash + /// The hashed password + public string HashPassword(string password) + { + string hashedPassword = PasswordHasher.HashPassword(null, password); + return hashedPassword; + } + + /// + /// Used to validate the password without an identity user + /// Validation code is based on the default ValidatePasswordAsync code + /// Should return if validation is successful + /// + /// The password. + /// A representing whether validation was successful. + public async Task ValidatePasswordAsync(string password) + { + var errors = new List(); + var isValid = true; + foreach (IPasswordValidator v in PasswordValidators) { - _passwordGenerator = new PasswordGenerator(PasswordConfiguration); + IdentityResult result = await v.ValidateAsync(this, null, password); + if (!result.Succeeded) + { + if (result.Errors.Any()) + { + errors.AddRange(result.Errors); + } + + isValid = false; + } } - var password = _passwordGenerator.GeneratePassword(); - return password; + if (!isValid) + { + Logger.LogWarning(14, "Password validation failed: {errors}.", string.Join(";", errors.Select(e => e.Code))); + return IdentityResult.Failed(errors.ToArray()); + } + + return IdentityResult.Success; } /// diff --git a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs index 83eec6ad1e..9baeaab18e 100644 --- a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs @@ -1,7 +1,6 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; -using Umbraco.Core.Security; namespace Umbraco.Core.Security { diff --git a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs b/src/Umbraco.Infrastructure/Services/Implement/FileService.cs index acab6caae4..ff42a35a25 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/FileService.cs @@ -33,7 +33,7 @@ namespace Umbraco.Core.Services.Implement private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; - private const string PartialViewHeader = "@inherits Umbraco.Web.Common.Views.UmbracoViewPage"; + private const string PartialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"; private const string PartialViewMacroHeader = "@inherits Umbraco.Web.Common.Macros.PartialViewMacroPage"; public FileService(IScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 7288fa190a..bf82719741 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -13,6 +13,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; using Umbraco.Extensions; +using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Services.Implement { @@ -112,19 +113,20 @@ namespace Umbraco.Core.Services.Implement /// Email of the Member to create /// Name of the Member to create /// Alias of the MemberType the Member should be based on + /// Thrown when a member type for the given alias isn't found /// public IMember CreateMember(string username, string email, string name, string memberTypeAlias) { - var memberType = GetMemberType(memberTypeAlias); + IMemberType memberType = GetMemberType(memberTypeAlias); if (memberType == null) + { throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias)); + } var member = new Member(name, email.ToLower().Trim(), username, memberType); - using (var scope = ScopeProvider.CreateScope()) - { - CreateMember(scope, member, 0, false); - scope.Complete(); - } + using IScope scope = ScopeProvider.CreateScope(); + CreateMember(scope, member, 0, false); + scope.Complete(); return member; } @@ -315,7 +317,9 @@ namespace Umbraco.Core.Services.Implement // if saving is cancelled, media remains without an identity var saveEventArgs = new SaveEventArgs(member); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + { return; + } _memberRepository.Save(member); @@ -324,7 +328,9 @@ namespace Umbraco.Core.Services.Implement } if (withIdentity == false) + { return; + } Audit(AuditType.New, member.CreatorId, member.Id, $"Member '{member.Name}' was created with Id {member.Id}"); } @@ -804,11 +810,11 @@ namespace Umbraco.Core.Services.Implement /// public void Save(IMember member, bool raiseEvents = true) { - //trimming username and email to make sure we have no trailing space + // trimming username and email to make sure we have no trailing space member.Username = member.Username.Trim(); member.Email = member.Email.Trim(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(member); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) @@ -831,6 +837,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } + Audit(AuditType.Save, 0, member.Id); scope.Complete(); @@ -927,18 +934,28 @@ namespace Umbraco.Core.Services.Implement } } - public IEnumerable GetAllRoles() + /// + /// Returns a list of all member roles + /// + /// A list of member roles + + public IEnumerable GetAllRoles() { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.MemberTree); - return _memberGroupRepository.GetMany().Select(x => x.Name).Distinct(); + return _memberGroupRepository.GetMany().Distinct(); } } + /// + /// Returns a list of all member roles for a given member ID + /// + /// + /// A list of member roles public IEnumerable GetAllRoles(int memberId) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.MemberTree); var result = _memberGroupRepository.GetMemberGroupsForMember(memberId); @@ -948,17 +965,17 @@ namespace Umbraco.Core.Services.Implement public IEnumerable GetAllRoles(string username) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.MemberTree); - var result = _memberGroupRepository.GetMemberGroupsForMember(username); + IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username); return result.Select(x => x.Name).Distinct(); } } public IEnumerable GetAllRolesIds() { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.MemberTree); return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct(); @@ -967,27 +984,27 @@ namespace Umbraco.Core.Services.Implement public IEnumerable GetAllRolesIds(int memberId) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.MemberTree); - var result = _memberGroupRepository.GetMemberGroupsForMember(memberId); + IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(memberId); return result.Select(x => x.Id).Distinct(); } } public IEnumerable GetAllRolesIds(string username) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.MemberTree); - var result = _memberGroupRepository.GetMemberGroupsForMember(username); + IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username); return result.Select(x => x.Id).Distinct(); } } public IEnumerable GetMembersInRole(string roleName) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.MemberTree); return _memberRepository.GetByMemberGroup(roleName); @@ -996,7 +1013,7 @@ namespace Umbraco.Core.Services.Implement public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.MemberTree); return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType); @@ -1005,71 +1022,66 @@ namespace Umbraco.Core.Services.Implement public bool DeleteRole(string roleName, bool throwIfBeingUsed) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Cms.Core.Constants.Locks.MemberTree); if (throwIfBeingUsed) { // get members in role - var membersInRole = _memberRepository.GetByMemberGroup(roleName); + IEnumerable membersInRole = _memberRepository.GetByMemberGroup(roleName); if (membersInRole.Any()) + { throw new InvalidOperationException("The role " + roleName + " is currently assigned to members"); + } } - var query = Query().Where(g => g.Name == roleName); - var found = _memberGroupRepository.Get(query).ToArray(); + IQuery query = Query().Where(g => g.Name == roleName); + IMemberGroup[] found = _memberGroupRepository.Get(query).ToArray(); - foreach (var memberGroup in found) + foreach (IMemberGroup memberGroup in found) + { _memberGroupService.Delete(memberGroup); + } scope.Complete(); return found.Length > 0; } } - public void AssignRole(string username, string roleName) - { - AssignRoles(new[] { username }, new[] { roleName }); - } + public void AssignRole(string username, string roleName) => AssignRoles(new[] { username }, new[] { roleName }); public void AssignRoles(string[] usernames, string[] roleNames) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Cms.Core.Constants.Locks.MemberTree); - var ids = _memberGroupRepository.GetMemberIds(usernames); + int[] ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.AssignRoles(ids, roleNames); scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames), nameof(AssignedRoles)); scope.Complete(); } } - public void DissociateRole(string username, string roleName) - { - DissociateRoles(new[] { username }, new[] { roleName }); - } + public void DissociateRole(string username, string roleName) => DissociateRoles(new[] { username }, new[] { roleName }); public void DissociateRoles(string[] usernames, string[] roleNames) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Cms.Core.Constants.Locks.MemberTree); - var ids = _memberGroupRepository.GetMemberIds(usernames); + int[] ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.DissociateRoles(ids, roleNames); scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames), nameof(RemovedRoles)); scope.Complete(); } } - public void AssignRole(int memberId, string roleName) - { - AssignRoles(new[] { memberId }, new[] { roleName }); - } + public void AssignRole(int memberId, string roleName) => AssignRoles(new[] { memberId }, new[] { roleName }); public void AssignRoles(int[] memberIds, string[] roleNames) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Cms.Core.Constants.Locks.MemberTree); _memberGroupRepository.AssignRoles(memberIds, roleNames); @@ -1078,14 +1090,11 @@ namespace Umbraco.Core.Services.Implement } } - public void DissociateRole(int memberId, string roleName) - { - DissociateRoles(new[] { memberId }, new[] { roleName }); - } + public void DissociateRole(int memberId, string roleName) => DissociateRoles(new[] { memberId }, new[] { roleName }); public void DissociateRoles(int[] memberIds, string[] roleNames) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Cms.Core.Constants.Locks.MemberTree); _memberGroupRepository.DissociateRoles(memberIds, roleNames); @@ -1098,10 +1107,7 @@ namespace Umbraco.Core.Services.Implement #region Private Methods - private void Audit(AuditType type, int userId, int objectId, string message = null) - { - _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message)); - } + private void Audit(AuditType type, int userId, int objectId, string message = null) => _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message)); #endregion @@ -1156,12 +1162,15 @@ namespace Umbraco.Core.Services.Implement /// public MemberExportModel ExportMember(Guid key) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - var query = Query().Where(x => x.Key == key); - var member = _memberRepository.Get(query).FirstOrDefault(); + IQuery query = Query().Where(x => x.Key == key); + IMember member = _memberRepository.Get(query).FirstOrDefault(); - if (member == null) return null; + if (member == null) + { + return null; + } var model = new MemberExportModel { @@ -1185,11 +1194,14 @@ namespace Umbraco.Core.Services.Implement private static IEnumerable GetPropertyExportItems(IMember member) { - if (member == null) throw new ArgumentNullException(nameof(member)); + if (member == null) + { + throw new ArgumentNullException(nameof(member)); + } var exportProperties = new List(); - foreach (var property in member.Properties) + foreach (IProperty property in member.Properties) { var propertyExportModel = new MemberExportProperty { @@ -1217,15 +1229,14 @@ namespace Umbraco.Core.Services.Implement public void DeleteMembersOfType(int memberTypeId) { // note: no tree to manage here - - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Cms.Core.Constants.Locks.MemberTree); // TODO: What about content that has the contenttype as part of its composition? - var query = Query().Where(x => x.ContentTypeId == memberTypeId); + IQuery query = Query().Where(x => x.ContentTypeId == memberTypeId); - var members = _memberRepository.Get(query).ToArray(); + IMember[] members = _memberRepository.Get(query).ToArray(); var deleteEventArgs = new DeleteEventArgs(members); if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) @@ -1234,43 +1245,58 @@ namespace Umbraco.Core.Services.Implement return; } - foreach (var member in members) + foreach (IMember member in members) { // delete media // triggers the deleted event (and handles the files) DeleteLocked(scope, member); } + scope.Complete(); } } private IMemberType GetMemberType(IScope scope, string memberTypeAlias) { - if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias)); - if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); + if (memberTypeAlias == null) + { + throw new ArgumentNullException(nameof(memberTypeAlias)); + } + + if (string.IsNullOrWhiteSpace(memberTypeAlias)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); + } scope.ReadLock(Cms.Core.Constants.Locks.MemberTypes); - var memberType = _memberTypeRepository.Get(memberTypeAlias); + IMemberType memberType = _memberTypeRepository.Get(memberTypeAlias); if (memberType == null) + { throw new Exception($"No MemberType matching the passed in Alias: '{memberTypeAlias}' was found"); // causes rollback + } return memberType; } private IMemberType GetMemberType(string memberTypeAlias) { - if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias)); - if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); + if (memberTypeAlias == null) + { + throw new ArgumentNullException(nameof(memberTypeAlias)); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + if (string.IsNullOrWhiteSpace(memberTypeAlias)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); + } + + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { return GetMemberType(scope, memberTypeAlias); } } - - #endregion } } diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index f295805a55..a7a183b492 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -12,6 +12,7 @@ + diff --git a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs deleted file mode 100644 index 0d132c2c0a..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Umbraco.Cms.ModelsBuilder.Embedded -{ - /// - /// Indicates that a property implements a given property alias. - /// - /// And therefore it should not be generated. - [AttributeUsage(AttributeTargets.Property , AllowMultiple = false, Inherited = false)] - public class ImplementPropertyTypeAttribute : Attribute - { - public ImplementPropertyTypeAttribute(string alias) - { - Alias = alias; - } - - public string Alias { get; private set; } - } -} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj deleted file mode 100644 index 4b59b981f9..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - net5.0 - Library - latest - Umbraco.Cms.ModelsBuilder.Embedded - - - - bin\Release\Umbraco.ModelsBuilder.Embedded.xml - - - - - - - - - - - - - - - - <_Parameter1>Umbraco.Tests - - - <_Parameter1>Umbraco.Tests.UnitTests - - - <_Parameter1>Umbraco.Tests.Benchmarks - - - <_Parameter1>Umbraco.Tests.Integration - - - diff --git a/src/Umbraco.TestData/LoadTestController.cs b/src/Umbraco.TestData/LoadTestController.cs index 6cbe31d70e..817b7a1d76 100644 --- a/src/Umbraco.TestData/LoadTestController.cs +++ b/src/Umbraco.TestData/LoadTestController.cs @@ -1,14 +1,12 @@ using System; -using System.Threading; +using System.Configuration; +using System.Diagnostics; using System.Linq; -using System.Web.Mvc; -using Umbraco.Core.Services; -using Umbraco.Core.Models; +using System.Threading; using System.Web; using System.Web.Hosting; +using System.Web.Mvc; using System.Web.Routing; -using System.Diagnostics; -using System.Configuration; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; @@ -63,7 +61,7 @@ namespace Umbraco.TestData "; private static readonly string _containerTemplateText = @" -@inherits Umbraco.Web.Mvc.UmbracoViewPage +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ Layout = null; var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault(); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 22f1f883d0..c862708bbe 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -1,5 +1,6 @@ /// -import { DocumentTypeBuilder, ContentBuilder, AliasHelper } from 'umbraco-cypress-testhelpers'; +import {AliasHelper, ContentBuilder, DocumentTypeBuilder} from 'umbraco-cypress-testhelpers'; + context('Content', () => { beforeEach(() => { @@ -558,7 +559,7 @@ context('Content', () => { cy.saveDocumentType(pickerDocType); // Edit it the template to allow us to verify the rendered view. - cy.editTemplate(pickerDocTypeName, `@inherits Umbraco.Web.Mvc.UmbracoViewPage + cy.editTemplate(pickerDocTypeName, `@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Web.PublishedModels; @{ Layout = null; diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts index 068338f8fa..f664123d3b 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts @@ -1,5 +1,5 @@ /// -import { PartialViewBuilder } from "umbraco-cypress-testhelpers"; +import {PartialViewBuilder} from "umbraco-cypress-testhelpers"; context('Partial Views', () => { @@ -95,7 +95,7 @@ context('Partial Views', () => { // Build and save partial view const partialView = new PartialViewBuilder() .withName(name) - .withContent("@inherits Umbraco.Web.Mvc.UmbracoViewPage") + .withContent("@inherits UUmbraco.Cms.Web.Common.Views.UmbracoViewPage") .build(); cy.savePartialView(partialView); @@ -123,7 +123,7 @@ context('Partial Views', () => { const partialView = new PartialViewBuilder() .withName(name) - .withContent("@inherits Umbraco.Web.Mvc.UmbracoViewPage\n") + .withContent("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n") .build(); cy.savePartialView(partialView); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index c586384af7..3122c3ebf7 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -1,5 +1,5 @@ /// -import { TemplateBuilder } from 'umbraco-cypress-testhelpers'; +import {TemplateBuilder} from 'umbraco-cypress-testhelpers'; context('Templates', () => { @@ -54,7 +54,7 @@ context('Templates', () => { const template = new TemplateBuilder() .withName(name) - .withContent('@inherits Umbraco.Web.Mvc.UmbracoViewPage\n') + .withContent('@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n') .build(); cy.saveTemplate(template); @@ -87,7 +87,7 @@ context('Templates', () => { const template = new TemplateBuilder() .withName(name) - .withContent('@inherits Umbraco.Web.Mvc.UmbracoViewPage\n') + .withContent('@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n') .build(); cy.saveTemplate(template); diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index df0ef4600c..caed95ae52 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -156,6 +156,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .AddBackOfficeCore() .AddBackOfficeAuthentication() .AddBackOfficeIdentity() + .AddMembersIdentity() .AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme) .AddPreviewSupport() .AddMvcAndRazor(mvcBuilding: mvcBuilder => diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 4430f16533..7009df52c4 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -216,6 +216,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing .AddRuntimeMinifier() .AddBackOfficeAuthentication() .AddBackOfficeIdentity() + .AddMembersIdentity() .AddTestServices(TestHelper, GetAppCaches()); if (TestOptions.Mapper) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index e704b82bdd..b323abf472 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; @@ -24,7 +25,6 @@ using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Core.Serialization; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositories { @@ -95,7 +95,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.That(repository.Get("test"), Is.Not.Null); Assert.That(FileSystems.MvcViewsFileSystem.FileExists("test.cshtml"), Is.True); Assert.AreEqual( - @"@usingUmbraco.Cms.Web.Common.PublishedModels;@inheritsUmbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage@{Layout=null;}".StripWhitespace(), + @"@usingUmbraco.Cms.Web.Common.PublishedModels;@inheritsUmbraco.Cms.Web.Common.Views.UmbracoViewPage@{Layout=null;}".StripWhitespace(), template.Content.StripWhitespace()); } } @@ -123,7 +123,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.That(repository.Get("test2"), Is.Not.Null); Assert.That(FileSystems.MvcViewsFileSystem.FileExists("test2.cshtml"), Is.True); Assert.AreEqual( - "@usingUmbraco.Cms.Web.Common.PublishedModels;@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = \"test.cshtml\";}".StripWhitespace(), + "@usingUmbraco.Cms.Web.Common.PublishedModels;@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ Layout = \"test.cshtml\";}".StripWhitespace(), template2.Content.StripWhitespace()); } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/StandardMvc-Package.xml b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/StandardMvc-Package.xml index ee6f7cea4a..a4787dd570 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/StandardMvc-Package.xml +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/StandardMvc-Package.xml @@ -189,17 +189,17 @@ http://www.creativefounds.co.uk - @@ -1099,7 +1099,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Articles SW_Master -
- + @Html.Raw(Model.Content.GetPropertyValue("bodyText")) - +
    @foreach (var item in nodes.Skip((page - 1) * pageSize).Take(pageSize)) { @@ -1172,9 +1172,9 @@ Google Maps - A map macro that you can use within Rich Text Areas

    @item.GetPropertyValue("articleSummary") - +

    - + }
@@ -1185,7 +1185,7 @@ Google Maps - A map macro that you can use within Rich Text Areas @for (int p = 1; p < totalPages + 1; p++) { //string selected = (p == page) ? "selected" : String.Empty; - //
  • @p
  • + //
  • @p
  • @p if (p < totalPages) { @@ -1197,7 +1197,7 @@ Google Maps - A map macro that you can use within Rich Text Areas
    - + @Html.Partial("ContentPanels",@Model.Content) ]]>
    @@ -1207,13 +1207,13 @@ Google Maps - A map macro that you can use within Rich Text Areas ClientAreas SW_Master - x.IsVisible() && x.TemplateId > 0 && Umbraco.MemberHasAccess(x.Id, x.Path)); }
    - + - - + +
    ]]>
    @@ -1243,7 +1243,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Layout = "SW_Master.cshtml"; }
    - + ]]> @@ -1266,9 +1266,9 @@ Google Maps - A map macro that you can use within Rich Text Areas @{ Layout = "SW_Master.cshtml"; } - +
    -
      +
        @{ var nodeIds = Model.Content.GetPropertyValue("slideshow").ToString().Split(','); List slides = new List(); @@ -1313,7 +1313,7 @@ Google Maps - A map macro that you can use within Rich Text Areas
    - +
    @Html.Raw(Model.Content.GetPropertyValue("panelContent1").ToString()) @@ -1345,7 +1345,7 @@ Google Maps - A map macro that you can use within Rich Text Areas } }
    - + ]]> @@ -1420,13 +1420,13 @@ Google Maps - A map macro that you can use within Rich Text Areas page = 1; } } - + }
    @@ -1470,14 +1470,14 @@ Google Maps - A map macro that you can use within Rich Text Areas

    @Html.Raw(searchHiglight)

    - + } } @@ -1489,7 +1489,7 @@ Google Maps - A map macro that you can use within Rich Text Areas @for (int p = 1; p < totalPages + 1; p++) { //string selected = (p == page) ? "selected" : String.Empty; - //
  • @p
  • + //
  • @p
  • @p if (p < totalPages) { @@ -1514,7 +1514,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Layout = "SW_Master.cshtml"; }
    - + @helper traverse(IPublishedContent node) { - var cc = node.Children.Where(x=>x.IsVisible() && x.TemplateId > 0); + var cc = node.Children.Where(x=>x.IsVisible() && x.TemplateId > 0); if (cc.Count()>0) {
      @@ -1560,7 +1560,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Layout = "SW_Master.cshtml"; }
      - + ]]> @@ -1590,27 +1590,27 @@ Google Maps - A map macro that you can use within Rich Text Areas @Model.Content.GetPropertyValue("title") - - + + - + - +
    protected ILocalizedTextService LocalizedTextService { get; } + /// + /// Handles if the content for the specified ID isn't found + /// + /// The content ID to find + /// Whether to throw an exception + /// The error response protected NotFoundObjectResult HandleContentNotFound(object id) { ModelState.AddModelError("id", $"content with id: {id} was not found"); - var errorResponse = NotFound(ModelState); + NotFoundObjectResult errorResponse = NotFound(ModelState); + + return errorResponse; } @@ -90,7 +98,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers where TSaved : IContentSave { // map the property values - foreach (var propertyDto in dto.Properties) + foreach (ContentPropertyDto propertyDto in dto.Properties) { // get the property editor if (propertyDto.PropertyEditor == null) @@ -101,42 +109,53 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // get the value editor // nothing to save/map if it is readonly - var valueEditor = propertyDto.PropertyEditor.GetValueEditor(); - if (valueEditor.IsReadOnly) continue; + IDataValueEditor valueEditor = propertyDto.PropertyEditor.GetValueEditor(); + if (valueEditor.IsReadOnly) + { + continue; + } // get the property - var property = contentItem.PersistedContent.Properties[propertyDto.Alias]; + IProperty property = contentItem.PersistedContent.Properties[propertyDto.Alias]; // prepare files, if any matching property and culture - var files = contentItem.UploadedFiles + ContentPropertyFile[] files = contentItem.UploadedFiles .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment) .ToArray(); - foreach (var file in files) + foreach (ContentPropertyFile file in files) + { file.FileName = file.FileName.ToSafeFileName(ShortStringHelper); + } // create the property data for the property editor var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType.Configuration) { ContentKey = contentItem.PersistedContent.Key, PropertyTypeKey = property.PropertyType.Key, - Files = files + Files = files }; // let the editor convert the value that was received, deal with files, etc - var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property)); + object value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property)); // set the value - tags are special - var tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); + TagsPropertyEditorAttribute tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); if (tagAttribute != null) { - var tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType.Configuration); - if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter; + TagConfiguration tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType.Configuration); + if (tagConfiguration.Delimiter == default) + { + tagConfiguration.Delimiter = tagAttribute.Delimiter; + } + var tagCulture = property.PropertyType.VariesByCulture() ? culture : null; property.SetTagsValue(_serializer, value, tagConfiguration, tagCulture); } else + { savePropertyValue(contentItem, property, value); + } } } @@ -153,38 +172,45 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// protected TPersisted GetObjectFromRequest(Func getFromService) { - //checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return + // checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return // it from the callback return HttpContext.Items.ContainsKey(typeof(TPersisted).ToString()) && HttpContext.Items[typeof(TPersisted).ToString()] != null - ? (TPersisted) HttpContext.Items[typeof (TPersisted).ToString()] + ? (TPersisted)HttpContext.Items[typeof(TPersisted).ToString()] : getFromService(); } /// /// Returns true if the action passed in means we need to create something new /// - /// - /// - internal static bool IsCreatingAction(ContentSaveAction action) - { - return (action.ToString().EndsWith("New")); - } + /// The content action + /// Returns true if this is a creating action + internal static bool IsCreatingAction(ContentSaveAction action) => action.ToString().EndsWith("New"); - protected void AddCancelMessage(INotificationModel display, - string header = "speechBubbles/operationCancelledHeader", - string message = "speechBubbles/operationCancelledText", - bool localizeHeader = true, + /// + /// Adds a cancelled message to the display + /// + /// + /// + /// + /// + /// + /// + /// + protected void AddCancelMessage(INotificationModel display, string header = "speechBubbles/operationCancelledHeader", string message = "speechBubbles/operationCancelledText", bool localizeHeader = true, bool localizeMessage = true, string[] headerParams = null, string[] messageParams = null) { - //if there's already a default event message, don't add our default one - var msgs = EventMessages; - if (msgs != null && msgs.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) return; + // if there's already a default event message, don't add our default one + IEventMessagesFactory messages = EventMessages; + if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) + { + return; + } display.AddWarningNotification( localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header, - localizeMessage ? LocalizedTextService.Localize(message, messageParams): message); + localizeMessage ? LocalizedTextService.Localize(message, messageParams) : message); } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 441ed1c5c3..25d83d1031 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -8,9 +8,9 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.ContentApps; @@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; @@ -31,6 +32,7 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Filters; +using Umbraco.Core.Security; using Umbraco.Core.Services.Implement; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -46,46 +48,72 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [OutgoingNoHyphenGuidFormat] public class MemberController : ContentControllerBase { - private readonly MemberPasswordConfigurationSettings _passwordConfig; private readonly PropertyEditorCollection _propertyEditors; - private readonly LegacyPasswordSecurity _passwordSecurity; private readonly UmbracoMapper _umbracoMapper; private readonly IMemberService _memberService; private readonly IMemberTypeService _memberTypeService; + private readonly IMemberManager _memberManager; private readonly IDataTypeService _dataTypeService; private readonly ILocalizedTextService _localizedTextService; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IJsonSerializer _jsonSerializer; + private readonly IShortStringHelper _shortStringHelper; + /// + /// Initializes a new instance of the class. + /// + /// The culture dictionary + /// The logger factory + /// The string helper + /// The event messages factory + /// The entry point for localizing key services + /// The property editors + /// The mapper + /// The member service + /// The member type service + /// The member manager + /// The data-type service + /// The back office security accessor + /// The JSON serializer public MemberController( ICultureDictionary cultureDictionary, ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IEventMessagesFactory eventMessages, ILocalizedTextService localizedTextService, - IOptions passwordConfig, PropertyEditorCollection propertyEditors, - LegacyPasswordSecurity passwordSecurity, UmbracoMapper umbracoMapper, IMemberService memberService, IMemberTypeService memberTypeService, + IMemberManager memberManager, IDataTypeService dataTypeService, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IJsonSerializer jsonSerializer) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer) { - _passwordConfig = passwordConfig.Value; _propertyEditors = propertyEditors; - _passwordSecurity = passwordSecurity; _umbracoMapper = umbracoMapper; _memberService = memberService; _memberTypeService = memberTypeService; + _memberManager = memberManager; _dataTypeService = dataTypeService; _localizedTextService = localizedTextService; - _backofficeSecurityAccessor = backofficeSecurityAccessor; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _jsonSerializer = jsonSerializer; + _shortStringHelper = shortStringHelper; } + /// + /// The paginated list of members + /// + /// The page number to display + /// The size of the page + /// The ordering of the member list + /// The direction of the member list + /// The system field to order by + /// The current filter for the list + /// The member type + /// The paged result of members public PagedResult GetPagedResults( int pageNumber = 1, int pageSize = 100, @@ -101,8 +129,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); } - var members = _memberService - .GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); + IMember[] members = _memberService.GetAll( + pageNumber - 1, + pageSize, + out var totalRecords, + orderBy, + orderDirection, + orderBySystemField, + memberTypeAlias, + filter).ToArray(); if (totalRecords == 0) { return new PagedResult(0, 0, 0); @@ -110,8 +145,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { - Items = members - .Select(x => _umbracoMapper.Map(x)) + Items = members.Select(x => _umbracoMapper.Map(x)) }; return pagedResult; } @@ -119,15 +153,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Returns a display node with a list view to render members /// - /// - /// + /// The member type to list + /// The member list for display public MemberListDisplay GetListNodeDisplay(string listName) { - var foundType = _memberTypeService.Get(listName); - var name = foundType != null ? foundType.Name : listName; + IMemberType foundType = _memberTypeService.Get(listName); + string name = foundType != null ? foundType.Name : listName; var apps = new List(); - apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Constants.DataTypes.DefaultMembersListView)); + apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView)); apps[0].Active = true; var display = new MemberListDisplay @@ -148,138 +182,130 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Gets the content json for the member /// - /// - /// + /// The Guid key of the member + /// The member for display [OutgoingEditorModelEvent] public MemberDisplay GetByKey(Guid key) { - var foundMember = _memberService.GetByKey(key); + IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { HandleContentNotFound(key); } + return _umbracoMapper.Map(foundMember); } /// /// Gets an empty content item for the /// - /// - /// + /// The content type + /// The empty member for display [OutgoingEditorModelEvent] public ActionResult GetEmpty(string contentTypeAlias = null) { - IMember emptyContent; if (contentTypeAlias == null) { return NotFound(); } - var contentType = _memberTypeService.Get(contentTypeAlias); + IMemberType contentType = _memberTypeService.Get(contentTypeAlias); if (contentType == null) { return NotFound(); } - var passwordGenerator = new PasswordGenerator(_passwordConfig); + string newPassword = _memberManager.GeneratePassword(); - emptyContent = new Member(contentType); - emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword(); + IMember emptyContent = new Member(contentType); + emptyContent.AdditionalData["NewPassword"] = newPassword; return _umbracoMapper.Map(emptyContent); } /// /// Saves member /// - /// + /// The content item to save as a member + /// The resulting member display object [FileUploadCleanupFilter] [OutgoingEditorModelEvent] [MemberSaveValidation] - public async Task> PostSave( - [ModelBinder(typeof(MemberBinder))] - MemberSave contentItem) + public async Task> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { + if (contentItem == null) + { + throw new ArgumentNullException("The member content item was null"); + } - //If we've reached here it means: + // If we've reached here it means: // * Our model has been bound // * and validated // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid - //map the properties to the persisted entity + // map the properties to the persisted entity MapPropertyValues(contentItem); await ValidateMemberDataAsync(contentItem); - //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors + // Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors if (ModelState.IsValid == false) { - var forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); + MemberDisplay forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); return new ValidationErrorResult(forDisplay); } - //We're gonna look up the current roles now because the below code can cause - // events to be raised and developers could be manually adding roles to members in - // their handlers. If we don't look this up now there's a chance we'll just end up - // removing the roles they've assigned. - var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); - //find the ones to remove and remove them - var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray(); - - //Depending on the action we need to first do a create or update using the membership provider + // Depending on the action we need to first do a create or update using the membership manager // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. + switch (contentItem.Action) { case ContentSaveAction.Save: - UpdateMemberData(contentItem); + ActionResult updateSuccessful = await UpdateMemberAsync(contentItem); + if (!(updateSuccessful.Result is null)) + { + return updateSuccessful.Result; + } + break; case ContentSaveAction.SaveNew: - contentItem.PersistedContent = CreateMemberData(contentItem); + ActionResult createSuccessful = await CreateMemberAsync(contentItem); + if (!(createSuccessful.Result is null)) + { + return createSuccessful.Result; + } + break; default: - //we don't support anything else for members + // we don't support anything else for members return NotFound(); } - //TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope + // TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope // but it would be nicer to have this taken care of within the Save method itself - //create/save the IMember - _memberService.Save(contentItem.PersistedContent); + // return the updated model + MemberDisplay display = _umbracoMapper.Map(contentItem.PersistedContent); - //Now let's do the role provider stuff - now that we've saved the content item (that is important since - // if we are changing the username, it must be persisted before looking up the member roles). - if (rolesToRemove.Any()) - { - _memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); - } - //find the ones to add and add them - var toAdd = contentItem.Groups.Except(currRoles).ToArray(); - if (toAdd.Any()) - { - //add the ones submitted - _memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); - } - - //return the updated model - var display = _umbracoMapper.Map(contentItem.PersistedContent); - - //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 + // lastly, if it is not valid, add the model state to the outgoing object and throw a 403 if (!ModelState.IsValid) { display.Errors = ModelState.ToErrorDictionary(); return new ValidationErrorResult(display, StatusCodes.Status403Forbidden); } - var localizedTextService = _localizedTextService; - //put the correct messages in + ILocalizedTextService localizedTextService = _localizedTextService; + + // put the correct messages in switch (contentItem.Action) { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); + display.AddSuccessNotification( + localizedTextService.Localize("speechBubbles/editMemberSaved"), + localizedTextService.Localize("speechBubbles/editMemberSaved")); break; } @@ -289,81 +315,121 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Maps the property values to the persisted entity /// - /// + /// The member content item to map properties from private void MapPropertyValues(MemberSave contentItem) { - UpdateName(contentItem); + // Don't update the name if it is empty + if (contentItem.Name.IsNullOrWhiteSpace() == false) + { + contentItem.PersistedContent.Name = contentItem.Name; + } - //map the custom properties - this will already be set for new entities in our member binder + // map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.Email = contentItem.Email; contentItem.PersistedContent.Username = contentItem.Username; - //use the base method to map the rest of the properties - base.MapPropertyValuesForPersistence( + // use the base method to map the rest of the properties + MapPropertyValuesForPersistence( contentItem, contentItem.PropertyCollectionDto, - (save, property) => property.GetValue(), //get prop val - (save, property, v) => property.SetValue(v), //set prop val + (save, property) => property.GetValue(), // get prop val + (save, property, v) => property.SetValue(v), // set prop val null); // member are all invariant } - private IMember CreateMemberData(MemberSave contentItem) + /// + /// Create a member from the supplied member content data + /// + /// All member password processing and creation is done via the identity manager + /// + /// Member content data + /// The identity result of the created member + private async Task> CreateMemberAsync(MemberSave contentItem) { - throw new NotImplementedException("Members have not been migrated to netcore"); + IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); + if (memberType == null) + { + throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); + } - // TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet. + var identityMember = MembersIdentityUser.CreateNew( + contentItem.Username, + contentItem.Email, + memberType.Alias, + contentItem.Name); - //var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); - //if (memberType == null) - // throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); - //var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true) - //{ - // CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id, - // RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword), - // Comments = contentItem.Comments, - // IsApproved = contentItem.IsApproved - //}; + IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); - //return member; + if (created.Succeeded == false) + { + return new ValidationErrorResult(created.Errors.ToErrorMessage()); + } + + // now re-look up the member, which will now exist + IMember member = _memberService.GetByEmail(contentItem.Email); + + // map the save info over onto the user + member = _umbracoMapper.Map(contentItem, member); + + int creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + member.CreatorId = creatorId; + + // assign the mapped property values that are not part of the identity properties + string[] builtInAliases = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray(); + foreach (ContentPropertyBasic property in contentItem.Properties) + { + if (builtInAliases.Contains(property.Alias) == false) + { + member.Properties[property.Alias].SetValue(property.Value); + } + } + + //TODO: do we need to resave the key? + //contentItem.PersistedContent.Key = contentItem.Key; + + // now the member has been saved via identity, resave the member with mapped content properties + _memberService.Save(member); + contentItem.PersistedContent = member; + + await AddOrUpdateRoles(contentItem, identityMember); + return true; } /// /// Update the member security data - /// - /// - /// /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. - /// - private void UpdateMemberData(MemberSave contentItem) + ///
    + /// The member to save + private async Task> UpdateMemberAsync(MemberSave contentItem) { - contentItem.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + contentItem.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types // have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted. // There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut // but we will take care of this in a generic way below so that it works for all props. - if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData()) + if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData()) { - var memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId); + IMemberType memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId); var sensitiveProperties = memberType .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) .ToList(); - foreach (var sensitiveProperty in sensitiveProperties) + foreach (IPropertyType sensitiveProperty in sensitiveProperties) { - var destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); + ContentPropertyBasic destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); if (destProp != null) { - //if found, change the value of the contentItem model to the persisted value so it remains unchanged - var origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias); + // if found, change the value of the contentItem model to the persisted value so it remains unchanged + object origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias); destProp.Value = origValue; } } } - var isLockedOut = contentItem.IsLockedOut; + bool isLockedOut = contentItem.IsLockedOut; - //if they were locked but now they are trying to be unlocked + // if they were locked but now they are trying to be unlocked if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false) { contentItem.PersistedContent.IsLockedOut = false; @@ -371,90 +437,153 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } else if (!contentItem.PersistedContent.IsLockedOut && isLockedOut) { - //NOTE: This should not ever happen unless someone is mucking around with the request data. - //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them + // NOTE: This should not ever happen unless someone is mucking around with the request data. + // An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them ModelState.AddModelError("custom", "An admin cannot lock a user"); } - //no password changes then exit ? - if (contentItem.Password == null) - return; - - throw new NotImplementedException("Members have not been migrated to netcore"); - // TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet. - // set the password - //contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword); - } - - private static void UpdateName(MemberSave memberSave) - { - //Don't update the name if it is empty - if (memberSave.Name.IsNullOrWhiteSpace() == false) + MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); + if (identityMember == null) { - memberSave.PersistedContent.Name = memberSave.Name; + return new ValidationErrorResult("Member was not found"); } + + if (contentItem.Password != null) + { + IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); + if (validatePassword.Succeeded == false) + { + return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage()); + } + + string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword); + identityMember.PasswordHash = newPassword; + contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; + if (identityMember.LastPasswordChangeDateUtc != null) + { + contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow; + identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate; + } + } + + IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); + + if (updatedResult.Succeeded == false) + { + return new ValidationErrorResult(updatedResult.Errors.ToErrorMessage()); + } + + _memberService.Save(contentItem.PersistedContent); + + await AddOrUpdateRoles(contentItem, identityMember); + return true; } - // TODO: This logic should be pulled into the service layer private async Task ValidateMemberDataAsync(MemberSave contentItem) { if (contentItem.Name.IsNullOrWhiteSpace()) { ModelState.AddPropertyError( - new ValidationResult("Invalid user name", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + new ValidationResult("Invalid user name", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); return false; } if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) { - //TODO implement when NETCORE members are implemented - throw new NotImplementedException("TODO implement when members are implemented"); - // var validPassword = await _passwordValidator.ValidateAsync(_passwordConfig, contentItem.Password.NewPassword); - // if (!validPassword) - // { - // ModelState.AddPropertyError( - // new ValidationResult("Invalid password: " + string.Join(", ", validPassword.Result), new[] { "value" }), - // string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - // return false; - // } + IdentityResult validPassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); + if (!validPassword.Succeeded) + { + ModelState.AddPropertyError( + new ValidationResult("Invalid password: " + MapErrors(validPassword.Errors), new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + return false; + } } - var byUsername = _memberService.GetByUsername(contentItem.Username); + IMember byUsername = _memberService.GetByUsername(contentItem.Username); if (byUsername != null && byUsername.Key != contentItem.Key) { ModelState.AddPropertyError( - new ValidationResult("Username is already in use", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + new ValidationResult("Username is already in use", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); return false; } - var byEmail = _memberService.GetByEmail(contentItem.Email); + IMember byEmail = _memberService.GetByEmail(contentItem.Email); if (byEmail != null && byEmail.Key != contentItem.Key) { ModelState.AddPropertyError( - new ValidationResult("Email address is already in use", new[] { "value" }), - string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + new ValidationResult("Email address is already in use", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); return false; } return true; } + private string MapErrors(IEnumerable result) + { + var sb = new StringBuilder(); + IEnumerable identityErrors = result.ToList(); + foreach (IdentityError error in identityErrors) + { + string errorString = $"{error.Description}"; + sb.AppendLine(errorString); + } + + return sb.ToString(); + } + + /// + /// Add or update the identity roles + /// + /// The member content item + /// The member as an identity user + private async Task AddOrUpdateRoles(MemberSave contentItem, MembersIdentityUser identityMember) + { + // We're gonna look up the current roles now because the below code can cause + // events to be raised and developers could be manually adding roles to members in + // their handlers. If we don't look this up now there's a chance we'll just end up + // removing the roles they've assigned. + IEnumerable currentRoles = await _memberManager.GetRolesAsync(identityMember); + + // find the ones to remove and remove them + IEnumerable roles = currentRoles.ToList(); + string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray(); + + // Now let's do the role provider stuff - now that we've saved the content item (that is important since + // if we are changing the username, it must be persisted before looking up the member roles). + if (rolesToRemove.Any()) + { + IdentityResult rolesIdentityResult = await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove); + } + + // find the ones to add and add them + string[] toAdd = contentItem.Groups.Except(roles).ToArray(); + if (toAdd.Any()) + { + // add the ones submitted + IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, toAdd); + } + } + /// /// Permanently deletes a member /// - /// - /// + /// Guid of the member to delete + /// The result of the deletion /// [HttpPost] public IActionResult DeleteByKey(Guid key) { - var foundMember = _memberService.GetByKey(key); + //TODO: move to MembersUserStore + IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { return HandleContentNotFound(key); } + _memberService.Delete(foundMember); return Ok(); @@ -468,25 +597,27 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [HttpGet] public IActionResult ExportMemberData(Guid key) { - var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; if (currentUser.HasAccessToSensitiveData() == false) { return Forbid(); } - var member = ((MemberService)_memberService).ExportMember(key); - if (member is null) throw new NullReferenceException("No member found with key " + key); + MemberExportModel member = ((MemberService)_memberService).ExportMember(key); + if (member is null) + { + throw new NullReferenceException("No member found with key " + key); + } var json = _jsonSerializer.Serialize(member); var fileName = $"{member.Name}_{member.Email}.txt"; + // Set custom header so umbRequestHelper.downloadFile can save the correct filename HttpContext.Response.Headers.Add("x-filename", fileName); - return File( Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName); + return File(Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName); } } - - } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs index 574af724c7..b8caf9751a 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs @@ -57,7 +57,7 @@ namespace Umbraco.Extensions services.TryAddScoped, UserClaimsPrincipalFactory>(); // CUSTOM: - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddSingleton(); @@ -69,7 +69,7 @@ namespace Umbraco.Extensions * To validate the container the following registrations are required (dependencies of UserManager) * Perhaps we shouldn't be registering UserManager at all and only registering/depending the UmbracoBackOffice prefixed types. */ - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); return new BackOfficeIdentityBuilder(services); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 4baf6a29f5..d120ee7c64 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.Middleware; +using Umbraco.Cms.Web.BackOffice.ModelsBuilder; using Umbraco.Cms.Web.BackOffice.Routing; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.BackOffice.Services; @@ -37,13 +38,15 @@ namespace Umbraco.Extensions .AddBackOfficeCore() .AddBackOfficeAuthentication() .AddBackOfficeIdentity() + .AddMembersIdentity() .AddBackOfficeAuthorizationPolicies() .AddUmbracoProfiler() .AddMvcAndRazor() .AddWebServer() .AddPreviewSupport() .AddHostedServices() - .AddDistributedCache(); + .AddDistributedCache() + .AddModelsBuilderDashboard(); /// /// Adds Umbraco back office authentication requirements @@ -92,6 +95,16 @@ namespace Umbraco.Extensions return builder; } + /// + /// Adds Identity support for Umbraco members + /// + public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) + { + builder.Services.AddMembersIdentity(); + + return builder; + } + /// /// Adds Umbraco back office authorization policies /// diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index c17d834226..e88503794e 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -94,14 +94,11 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping target.Path = $"-1,{source.Id}"; target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key); } - + // Umbraco.Code.MapAll private static void Map(IMember source, ContentPropertyCollectionDto target, MapperContext context) { target.Properties = context.MapEnumerable(source.Properties); } - - - } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidator.cs similarity index 85% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidator.cs index 9b0e87247e..5a73c7e3fe 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidator.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { /// /// Used to validate the aliases for the content type when MB is enabled to ensure that diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidatorBase.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidatorBase.cs index 3978b54648..40ccd1f60b 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidatorBase.cs @@ -10,7 +10,7 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { public abstract class ContentTypeModelValidatorBase : EditorValidator where TModel : ContentTypeSave diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/DashboardReport.cs similarity index 96% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/DashboardReport.cs index 940ec1461f..9620e7c08f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/DashboardReport.cs @@ -2,10 +2,11 @@ using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Infrastructure.ModelsBuilder; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { internal class DashboardReport { diff --git a/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs similarity index 85% rename from src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs index 060e0f4bfa..dad8e54960 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs @@ -1,9 +1,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Features; -using Umbraco.Cms.ModelsBuilder.Embedded.BackOffice; -using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { /// /// Used in conjunction with @@ -17,10 +15,8 @@ namespace Umbraco.Cms.ModelsBuilder.Embedded /// /// Handles the notification to disable MB controller features /// - public void Handle(UmbracoApplicationStarting notification) - { + public void Handle(UmbracoApplicationStarting notification) => // disable the embedded dashboard controller _features.Disabled.Controllers.Add(); - } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/MediaTypeModelValidator.cs similarity index 85% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/MediaTypeModelValidator.cs index 552c3f143c..e3a99de492 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/MediaTypeModelValidator.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { /// /// Used to validate the aliases for the content type when MB is enabled to ensure that diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/MemberTypeModelValidator.cs similarity index 85% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/MemberTypeModelValidator.cs index 283b8a8d9d..995b0616f0 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/MemberTypeModelValidator.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { /// /// Used to validate the aliases for the content type when MB is enabled to ensure that diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardController.cs similarity index 97% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardController.cs index 8e7a4b989c..07000c069c 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardController.cs @@ -5,12 +5,13 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.ModelsBuilder.Embedded.Building; +using Umbraco.Cms.Infrastructure.ModelsBuilder; +using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; -namespace Umbraco.Cms.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { /// /// API controller for use in the Umbraco back office with Angular resources diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs new file mode 100644 index 0000000000..6d1a7a13fc --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Routing; +using Umbraco.Extensions; +using Umbraco.Web.Common.ModelsBuilder; + +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder +{ + public class ModelsBuilderDashboardProvider: IModelsBuilderDashboardProvider + { + private readonly LinkGenerator _linkGenerator; + + public ModelsBuilderDashboardProvider(LinkGenerator linkGenerator) + { + _linkGenerator = linkGenerator; + } + + public string GetUrl() => + _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => + controller.BuildModels()); + } +} diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..4e372d8e62 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Extensions; +using Umbraco.Web.Common.ModelsBuilder; + +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder +{ + /// + /// Extension methods for for the common Umbraco functionality + /// + public static class UmbracoBuilderExtensions + { + /// + /// Adds the ModelsBuilder dashboard. + /// + public static IUmbracoBuilder AddModelsBuilderDashboard(this IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + return builder; + } + + /// + /// Can be called if using an external models builder to remove the embedded models builder controller features + /// + public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + return builder; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs index 17190b1b37..3ce89fc56f 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs index 9037f39da1..9d04086513 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs index ea8a0dcfc9..e6292d3686 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; diff --git a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs index 44a83cfeb8..2eb6fa229f 100644 --- a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Configuration.Models; diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs index 7b18f4b04f..b8fe18862e 100644 --- a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index 5d72327525..ad5f21b48e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -54,12 +54,14 @@ namespace Umbraco.Cms.Web.BackOffice.Trees /// /// Gets an individual tree node /// - /// - /// - /// - public ActionResult GetTreeNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + public ActionResult GetTreeNode([FromRoute]string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) { - var node = GetSingleTreeNode(id, queryStrings); + ActionResult node = GetSingleTreeNode(id, queryStrings); + + if (!(node.Result is null)) + { + return node.Result; + } //add the tree alias to the node since it is standalone (has no root for which this normally belongs) node.Value.AdditionalData["treeAlias"] = TreeAlias; diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index b764dbec40..1c12390ada 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.Commands; @@ -9,6 +11,8 @@ using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Processors; using SixLabors.ImageSharp.Web.Providers; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Core.Security; +using Umbraco.Web.Common.Security; namespace Umbraco.Extensions { @@ -55,6 +59,25 @@ namespace Umbraco.Extensions return services; } + /// + /// Adds the services required for using Members Identity + /// + public static void AddMembersIdentity(this IServiceCollection services) => + services.BuildMembersIdentity() + .AddDefaultTokenProviders() + .AddUserStore() + .AddMembersManager(); + + + private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services) + { + // Services used by Umbraco members identity + services.TryAddScoped, UserValidator>(); + services.TryAddScoped, PasswordValidator>(); + services.TryAddScoped, PasswordHasher>(); + return new MembersIdentityBuilder(services); + } + private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) { if (commands.TryGetValue(parameter, out var command)) diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs new file mode 100644 index 0000000000..1491002ae2 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Security; + +namespace Umbraco.Extensions +{ + /// + /// Extension methods for + /// + public static class IdentityBuilderExtensions + { + /// + /// Adds a for the . + /// + /// The usermanager interface + /// The usermanager type + /// The current instance. + public static IdentityBuilder AddMembersManager(this IdentityBuilder identityBuilder) + where TUserManager : UserManager, TInterface + { + identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); + return identityBuilder; + } + } +} diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroPage.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroPage.cs index 2c8a77bd05..84b9e89051 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroPage.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroPage.cs @@ -1,5 +1,5 @@ using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Web.Common.AspNetCore; +using Umbraco.Cms.Web.Common.Views; namespace Umbraco.Cms.Web.Common.Macros { diff --git a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs index 718d2f3a4c..476bb272b2 100644 --- a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Exceptions; diff --git a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs similarity index 94% rename from src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs rename to src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs index 5209683e8e..7cbbf3c9c7 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs @@ -1,3 +1,4 @@ +using System.Linq; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -7,9 +8,11 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.ModelsBuilder.Embedded; -using Umbraco.Cms.ModelsBuilder.Embedded.Building; +using Umbraco.Cms.Infrastructure.ModelsBuilder; +using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Cms.Web.Common.ModelBinders; +using Umbraco.Cms.Web.Common.ModelsBuilder; +using Umbraco.Web.Common.ModelsBuilder; using Umbraco.Web.WebAssets; /* @@ -75,7 +78,7 @@ namespace Umbraco.Extensions /// /// Extension methods for for the common Umbraco functionality /// - public static class UmbracoBuilderExtensions + public static class UmbracoBuilderDependencyInjectionExtensions { /// /// Adds umbraco's embedded model builder support @@ -123,15 +126,12 @@ namespace Umbraco.Extensions } }); - return builder; - } - /// - /// Can be called if using an external models builder to remove the embedded models builder controller features - /// - public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); + if (!builder.Services.Any(x=>x.ServiceType == typeof(IModelsBuilderDashboardProvider))) + { + builder.Services.AddUnique(); + } + return builder; } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs new file mode 100644 index 0000000000..47af1d2a94 --- /dev/null +++ b/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Common.ModelsBuilder +{ + public interface IModelsBuilderDashboardProvider + { + string GetUrl(); + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs b/src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs similarity index 94% rename from src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs rename to src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs index a8b9ac3b14..146f5f577e 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; @@ -10,13 +9,13 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.ModelsBuilder.Embedded.BackOffice; +using Umbraco.Cms.Infrastructure.ModelsBuilder; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Core.Services.Implement; -using Umbraco.Extensions; +using Umbraco.Web.Common.ModelsBuilder; using Umbraco.Web.WebAssets; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Web.Common.ModelsBuilder { /// /// Handles and notifications to initialize MB @@ -25,17 +24,16 @@ namespace Umbraco.Cms.ModelsBuilder.Embedded { private readonly ModelsBuilderSettings _config; private readonly IShortStringHelper _shortStringHelper; - private readonly LinkGenerator _linkGenerator; + private readonly IModelsBuilderDashboardProvider _modelsBuilderDashboardProvider; public ModelsBuilderNotificationHandler( IOptions config, IShortStringHelper shortStringHelper, - LinkGenerator linkGenerator) + IModelsBuilderDashboardProvider modelsBuilderDashboardProvider) { _config = config.Value; _shortStringHelper = shortStringHelper; - _shortStringHelper = shortStringHelper; - _linkGenerator = linkGenerator; + _modelsBuilderDashboardProvider = modelsBuilderDashboardProvider; } /// @@ -84,7 +82,7 @@ namespace Umbraco.Cms.ModelsBuilder.Embedded throw new ArgumentException("Invalid umbracoPlugins"); } - umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoUrls["modelsBuilderBaseUrl"] = _modelsBuilderDashboardProvider.GetUrl(); umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs new file mode 100644 index 0000000000..bc9e9d32a7 --- /dev/null +++ b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Web.Common.ModelsBuilder +{ + public class NoopModelsBuilderDashboardProvider: IModelsBuilderDashboardProvider + { + public string GetUrl() => string.Empty; + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs similarity index 99% rename from src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs rename to src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs index ef01d41b2e..8a17419964 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs @@ -18,11 +18,12 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.ModelsBuilder.Embedded.Building; +using Umbraco.Cms.Infrastructure.ModelsBuilder; +using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; using File = System.IO.File; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Web.Common.ModelsBuilder { internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject { diff --git a/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs b/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs similarity index 99% rename from src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs rename to src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs index 0d6d37a119..048a6e2965 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; /* * OVERVIEW: - * + * * The CSharpCompiler is responsible for the actual compilation of razor at runtime. * It creates a CSharpCompilation instance to do the compilation. This is where DLL references * are applied. However, the way this works is not flexible for dynamic assemblies since the references @@ -16,31 +16,31 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; * RazorReferenceManager. Unfortunately this is also internal and cannot be replaced, though it can be extended * using MvcRazorRuntimeCompilationOptions, except this is the place where references are only loaded once which * is done with a LazyInitializer. See https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs#L35. - * + * * The way that RazorReferenceManager works is by resolving references from the ApplicationPartsManager - either by * an application part that is specifically an ICompilationReferencesProvider or an AssemblyPart. So to fulfill this * requirement, we add the MB assembly to the assembly parts manager within the PureLiveModelFactory when the assembly * is (re)generated. But due to the above restrictions, when re-generating, this will have no effect since the references * have already been resolved with the LazyInitializer in the RazorReferenceManager. - * + * * The services that can be replaced are: IViewCompilerProvider (default is the internal RuntimeViewCompilerProvider) and * IViewCompiler (default is the internal RuntimeViewCompiler). There is one specific public extension point that I was * hoping would solve all of the problems which was IMetadataReferenceFeature (implemented by LazyMetadataReferenceFeature * which uses RazorReferencesManager) which is a razor feature that you can add * to the RazorProjectEngine. It is used to resolve roslyn references and by default is backed by RazorReferencesManager. * Unfortunately, this service is not used by the CSharpCompiler, it seems to only be used by some tag helper compilations. - * + * * There are caches at several levels, all of which are not publicly accessible APIs (apart from RazorViewEngine.ViewLookupCache * which is possible to clear by casting and then calling cache.Compact(100); but that doesn't get us far enough). - * + * * For this to work, several caches must be cleared: * - RazorViewEngine.ViewLookupCache * - RazorReferencesManager._compilationReferences * - RazorPageActivator._activationInfo (though this one may be optional) * - RuntimeViewCompiler._cache - * + * * What are our options? - * + * * a) We can copy a ton of code into our application: CSharpCompiler, RuntimeViewCompilerProvider, RuntimeViewCompiler and * RazorReferenceManager (probably more depending on the extent of Internal references). * b) We can use reflection to try to access all of the above resources and try to forcefully clear caches and reset initialization flags. @@ -49,7 +49,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; * services from scratch which means there is no caches. * * ... Option C works, we will use that but need to verify how this affects memory since ideally the old services will be GC'd. - * + * * Option C, how its done: * - Before we add our custom razor services to the container, we make a copy of the services collection which is the snapshot of registered services * with razor defaults before ours are added. @@ -60,7 +60,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature. */ -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Web.Common.ModelsBuilder { /// /// Custom that wraps aspnetcore's default implementation diff --git a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs b/src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs similarity index 94% rename from src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs rename to src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs index eb4af946f7..e374a7292f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Runtime.Loader; -namespace Umbraco.Cms.ModelsBuilder.Embedded +namespace Umbraco.Cms.Web.Common.ModelsBuilder { internal class UmbracoAssemblyLoadContext : AssemblyLoadContext { diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 196843868e..18029228b4 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -28,13 +28,12 @@ namespace Umbraco.Cms.Web.Common.Security IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - BackOfficeLookupNormalizer keyNormalizer, BackOfficeIdentityErrorDescriber errors, IServiceProvider services, IHttpContextAccessor httpContextAccessor, ILogger> logger, IOptions passwordConfiguration) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration) + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) { _httpContextAccessor = httpContextAccessor; } @@ -138,7 +137,7 @@ namespace Umbraco.Cms.Web.Common.Security return result; } - + /// public override async Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd) { diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs new file mode 100644 index 0000000000..386b1ba231 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Security.Principal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Security; +using Umbraco.Core.Security; +using Umbraco.Extensions; + + +namespace Umbraco.Web.Common.Security +{ + public class MemberManager : UmbracoUserManager, IMemberManager + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public MemberManager( + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + BackOfficeIdentityErrorDescriber errors, + IServiceProvider services, + IHttpContextAccessor httpContextAccessor, + ILogger> logger, + IOptions passwordConfiguration) + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) + { + _httpContextAccessor = httpContextAccessor; + } + + private string GetCurrentUserId(IPrincipal currentUser) + { + UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); + var currentUserId = umbIdentity?.GetUserId() ?? Cms.Core.Constants.Security.SuperUserIdAsString; + return currentUserId; + } + + private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, string affectedUserId, string affectedUsername) + { + var currentUserId = GetCurrentUserId(currentUser); + var ip = IpResolver.GetCurrentRequestIpAddress(); + return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); + } + + //TODO: have removed all other member audit events - can revisit if we need member auditing on a user level in future + + public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); + + public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); + + public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); + + public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) => throw new NotImplementedException(); + + public bool HasSendingUserInviteEventHandler { get; } + } +} diff --git a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs index d96ed397c5..66d9b3c3f2 100644 --- a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs @@ -18,7 +18,7 @@ using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.Views { public abstract class UmbracoViewPage : UmbracoViewPage { diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 9e4c6de077..bfc3d31f8e 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -13,7 +13,6 @@ true - diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/blocklist/default.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/blocklist/default.cshtml index 38f7431f25..1c4f33b460 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/blocklist/default.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/blocklist/default.cshtml @@ -1,4 +1,4 @@ -@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ if (!Model.Any()) { return; } } diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3-fluid.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3-fluid.cshtml index 840c9c1218..8832f1752a 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3-fluid.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3-fluid.cshtml @@ -1,7 +1,7 @@ @using System.Web @using Microsoft.AspNetCore.Html @using Newtonsoft.Json.Linq -@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @* Razor helpers located at the bottom of this file diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3.cshtml index 892dc84afe..65790d31fb 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3.cshtml @@ -1,7 +1,7 @@ @using System.Web @using Microsoft.AspNetCore.Html @using Newtonsoft.Json.Linq -@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @if (Model != null && Model.sections != null) { diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/embed.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/embed.cshtml index dbd9438fa7..a383046420 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/embed.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/embed.cshtml @@ -1,5 +1,4 @@ -@using Umbraco.Cms.Core -@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ string embedValue = Convert.ToString(Model.value); diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/macro.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/macro.cshtml index 4305a1e4cf..8543d00209 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/macro.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/macro.cshtml @@ -1,4 +1,4 @@ -@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @if (Model.value != null) { diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json index a3e57978da..382ee11590 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.json @@ -71,4 +71,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.NetCore/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI.NetCore/config/BackOfficeTours/getting-started.json index 75797f78e7..f82ad96c55 100644 --- a/src/Umbraco.Web.UI.NetCore/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI.NetCore/config/BackOfficeTours/getting-started.json @@ -191,7 +191,9 @@ "element": "[ng-controller*='Umbraco.Editors.DataTypePickerController'] [data-element='editor-data-type-picker']", "elementPreventClick": true, "title": "Editor picker", - "content": "

    In the editor picker dialog we can pick one of the many built-in editors.

    " + "content": "

    In the editor picker dialog we can pick one of the many built-in editors.

    ", + + }, { "element": "[data-element~='editor-data-type-picker'] [data-element='datatype-Textarea']", diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj index c3b5a6b1c4..8b940ba47b 100644 --- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj +++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj @@ -22,7 +22,6 @@ - diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index e348aceaee..0ef8fcf488 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -12,7 +12,6 @@ using System.Runtime.InteropServices; // Umbraco Cms [assembly: InternalsVisibleTo("Umbraco.Web.UI")] -[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] diff --git a/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs index 7bd67e608a..0a547d9eb2 100644 --- a/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Umbraco.Core.Security; namespace Umbraco.Web.Security diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 961a8cd86d..4f48b5cc80 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -19,6 +19,15 @@ using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Web.Security { // MIGRATED TO NETCORE + // TODO: Analyse all - much can be moved/removed since most methods will occur on the manager via identity implementation + + /// + /// Helper class containing logic relating to the built-in Umbraco members macros and controllers for: + /// - Registration + /// - Updating + /// - Logging in + /// - Current status + /// public class MembershipHelper { private readonly MembersMembershipProvider _membershipProvider; @@ -118,7 +127,7 @@ namespace Umbraco.Web.Security var pathsWithAccess = HasAccess(pathsWithProtection, Roles.Provider); var result = new Dictionary(); - foreach(var path in paths) + foreach (var path in paths) { pathsWithAccess.TryGetValue(path, out var hasAccess); // if it's not found it's false anyways @@ -144,7 +153,8 @@ namespace Umbraco.Web.Security string[] userRoles = null; string[] getUserRoles(string username) { - if (userRoles != null) return userRoles; + if (userRoles != null) + return userRoles; userRoles = roleProvider.GetRolesForUser(username).ToArray(); return userRoles; } @@ -185,7 +195,8 @@ namespace Umbraco.Web.Security var provider = _membershipProvider; var membershipUser = provider.GetCurrentUser(); //NOTE: This should never happen since they are logged in - if (membershipUser == null) throw new InvalidOperationException("Could not find member with username " + _httpContextAccessor.GetRequiredHttpContext().User.Identity.Name); + if (membershipUser == null) + throw new InvalidOperationException("Could not find member with username " + _httpContextAccessor.GetRequiredHttpContext().User.Identity.Name); try { @@ -257,7 +268,8 @@ namespace Umbraco.Web.Security null, null, true, null, out status); - if (status != MembershipCreateStatus.Success) return null; + if (status != MembershipCreateStatus.Success) + return null; var member = _memberService.GetByUsername(membershipUser.UserName); member.Name = model.Name; @@ -367,7 +379,8 @@ namespace Umbraco.Web.Security public virtual IPublishedContent Get(Udi udi) { var guidUdi = udi as GuidUdi; - if (guidUdi == null) return null; + if (guidUdi == null) + return null; var umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(udi.EntityType); @@ -702,27 +715,32 @@ namespace Umbraco.Web.Security if (email != null) { - if (member.Email != email) update = true; + if (member.Email != email) + update = true; member.Email = email; } if (isApproved.HasValue) { - if (member.IsApproved != isApproved.Value) update = true; + if (member.IsApproved != isApproved.Value) + update = true; member.IsApproved = isApproved.Value; } if (lastLoginDate.HasValue) { - if (member.LastLoginDate != lastLoginDate.Value) update = true; + if (member.LastLoginDate != lastLoginDate.Value) + update = true; member.LastLoginDate = lastLoginDate.Value; } if (lastActivityDate.HasValue) { - if (member.LastActivityDate != lastActivityDate.Value) update = true; + if (member.LastActivityDate != lastActivityDate.Value) + update = true; member.LastActivityDate = lastActivityDate.Value; } if (comment != null) { - if (member.Comment != comment) update = true; + if (member.Comment != comment) + update = true; member.Comment = comment; } @@ -741,8 +759,8 @@ namespace Umbraco.Web.Security { var provider = _membershipProvider; - var username = provider.GetCurrentUserName(); - // The result of this is cached by the MemberRepository + var username = provider.GetCurrentUserName(); + // The result of this is cached by the MemberRepository var member = _memberService.GetByUsername(username); return member; } @@ -763,8 +781,10 @@ namespace Umbraco.Web.Security // YES! It is completely insane how many options you have to take into account based on the membership provider. yikes! - if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); - if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider)); + if (passwordModel == null) + throw new ArgumentNullException(nameof(passwordModel)); + if (membershipProvider == null) + throw new ArgumentNullException(nameof(membershipProvider)); var userId = -1; diff --git a/src/Umbraco.Web/Security/MembershipProviderBase.cs b/src/Umbraco.Web/Security/MembershipProviderBase.cs index d664dc527e..b0ddac250e 100644 --- a/src/Umbraco.Web/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Web/Security/MembershipProviderBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using System.ComponentModel.DataAnnotations; using System.Configuration.Provider; @@ -14,9 +14,11 @@ using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Web.Security { + //TODO: Delete - should not be used /// /// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing. /// + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] public abstract class MembershipProviderBase : MembershipProvider { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index 008d81623d..97ab5f9adc 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using System.Configuration.Provider; using System.Web.Security; @@ -15,6 +15,8 @@ using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Web.Security.Providers { + //TODO: Delete: should not be used + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] /// /// Custom Membership Provider for Umbraco Members (User authentication for Frontend applications NOT umbraco CMS) /// @@ -121,6 +123,7 @@ namespace Umbraco.Web.Security.Providers public override LegacyPasswordSecurity PasswordSecurity => _passwordSecurity.Value; public IPasswordConfiguration PasswordConfiguration => _passwordConfig.Value; + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] private class MembershipProviderPasswordConfiguration : IPasswordConfiguration { public MembershipProviderPasswordConfiguration(int requiredLength, bool requireNonLetterOrDigit, bool requireDigit, bool requireLowercase, bool requireUppercase, bool useLegacyEncoding, string hashAlgorithmType, int maxFailedAccessAttemptsBeforeLockout) diff --git a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs index 4ebc82b426..2aefd73f22 100644 --- a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs @@ -1,11 +1,10 @@ -using System.Collections.Specialized; +using System; using System.Configuration.Provider; using System.Linq; using System.Web.Security; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; -using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Services; @@ -13,9 +12,12 @@ using Umbraco.Web.Composing; namespace Umbraco.Web.Security.Providers { + //TODO: Delete: should not be used + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] public class MembersRoleProvider : RoleProvider { private readonly IMembershipRoleService _roleService; + private string _applicationName; public MembersRoleProvider(IMembershipRoleService roleService) { @@ -27,8 +29,6 @@ namespace Umbraco.Web.Security.Providers { } - private string _applicationName; - public override bool IsUserInRole(string username, string roleName) { return GetRolesForUser(username).Any(x => x == roleName); @@ -49,10 +49,12 @@ namespace Umbraco.Web.Security.Providers return _roleService.DeleteRole(roleName, throwOnPopulatedRole); } - public override bool RoleExists(string roleName) - { - return _roleService.GetAllRoles().Any(x => x == roleName); - } + /// + /// Returns true if the specified member role name exists + /// + /// Member role name + /// True if member role exists, otherwise false + public override bool RoleExists(string roleName) => _roleService.GetAllRoles().Any(x => x.Name == roleName); public override void AddUsersToRoles(string[] usernames, string[] roleNames) { @@ -69,10 +71,11 @@ namespace Umbraco.Web.Security.Providers return _roleService.GetMembersInRole(roleName).Select(x => x.Username).ToArray(); } - public override string[] GetAllRoles() - { - return _roleService.GetAllRoles().ToArray(); - } + /// + /// Gets all the member roles + /// + /// A list of member roles + public override string[] GetAllRoles() => _roleService.GetAllRoles().Select(x => x.Name).ToArray(); public override string[] FindUsersInRole(string roleName, string usernameToMatch) { @@ -90,6 +93,7 @@ namespace Umbraco.Web.Security.Providers { return _applicationName; } + set { if (string.IsNullOrEmpty(value)) diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs index 5c21e43b2a..68e4f9916d 100644 --- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using System.Configuration.Provider; using System.Linq; @@ -17,6 +17,8 @@ using Umbraco.Web.Composing; namespace Umbraco.Web.Security.Providers { + //TODO: Delete - should not be used + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] /// /// Abstract Membership Provider that users any implementation of IMembershipMemberService{TEntity} service /// @@ -30,7 +32,7 @@ namespace Umbraco.Web.Security.Providers protected IMembershipMemberService MemberService { get; private set; } protected UmbracoMembershipProvider(IMembershipMemberService memberService, IUmbracoVersion umbracoVersion, IHostingEnvironment hostingEnvironment, IIpResolver ipResolver) - :base(hostingEnvironment) + : base(hostingEnvironment) { _umbracoVersion = umbracoVersion; _ipResolver = ipResolver; @@ -53,9 +55,11 @@ namespace Umbraco.Web.Security.Providers /// The name of the provider has a length of zero. public override void Initialize(string name, NameValueCollection config) { - if (config == null) { throw new ArgumentNullException("config"); } + if (config == null) + { throw new ArgumentNullException("config"); } - if (string.IsNullOrEmpty(name)) name = ProviderName; + if (string.IsNullOrEmpty(name)) + name = ProviderName; // Initialize base provider class base.Initialize(name, config); @@ -78,7 +82,8 @@ namespace Umbraco.Web.Security.Providers // in order to support updating passwords from the umbraco core, we can't validate the old password var m = MemberService.GetByUsername(username); - if (m == null) return false; + if (m == null) + return false; string salt; var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, newPassword, out salt); @@ -172,7 +177,8 @@ namespace Umbraco.Web.Security.Providers public override bool DeleteUser(string username, bool deleteAllRelatedData) { var member = MemberService.GetByUsername(username); - if (member == null) return false; + if (member == null) + return false; MemberService.Delete(member); return true; @@ -421,7 +427,8 @@ namespace Umbraco.Web.Security.Providers } // Non need to update - if (member.IsLockedOut == false) return true; + if (member.IsLockedOut == false) + return true; member.IsLockedOut = false; member.FailedPasswordAttempts = 0; diff --git a/src/umbraco.sln b/src/umbraco.sln index b7f54cead9..840c7213e6 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -8,15 +8,15 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4-3B4E-40A3-A309-F3CB4F0E125F}" ProjectSection(SolutionItems) = preProject ..\linting\.editorconfig = ..\linting\.editorconfig + ..\build\azure-pipelines.yml = ..\build\azure-pipelines.yml ..\build\build-bootstrap.ps1 = ..\build\build-bootstrap.ps1 ..\build\build.ps1 = ..\build\build.ps1 - ..\NuGet.Config = ..\NuGet.Config - ..\linting\stylecop.json = ..\linting\stylecop.json ..\linting\codeanalysis.ruleset = ..\linting\codeanalysis.ruleset ..\linting\codeanalysis.tests.ruleset = ..\linting\codeanalysis.tests.ruleset ..\Directory.Build.props = ..\Directory.Build.props ..\Directory.Build.targets = ..\Directory.Build.targets - ..\build\azure-pipelines.yml = ..\build\azure-pipelines.yml + ..\NuGet.Config = ..\NuGet.Config + ..\linting\stylecop.json = ..\linting\stylecop.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{FD962632-184C-4005-A5F3-E705D92FC645}" @@ -39,8 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C ProjectSection(SolutionItems) = preProject ..\build\NuSpecs\UmbracoCms.Core.nuspec = ..\build\NuSpecs\UmbracoCms.Core.nuspec ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec - ..\build\NuSpecs\UmbracoCms.Web.nuspec = ..\build\NuSpecs\UmbracoCms.Web.nuspec ..\build\NuSpecs\UmbracoCms.SqlCe.nuspec = ..\build\NuSpecs\UmbracoCms.SqlCe.nuspec + ..\build\NuSpecs\UmbracoCms.Web.nuspec = ..\build\NuSpecs\UmbracoCms.Web.nuspec EndProjectSection EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}" @@ -90,11 +90,11 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTes Release.AspNetCompiler.ForceOverwrite = "true" Release.AspNetCompiler.FixedNames = "false" Release.AspNetCompiler.Debug = "False" + SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" DefaultWebSiteLanguage = "Visual C#" StartServerOnDebug = "false" VWDPort = "58896" VWDPort = "62926" - SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web", "Umbraco.Web\Umbraco.Web.csproj", "{651E1350-91B6-44B7-BD60-7207006D7003}" @@ -113,10 +113,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E3F9F378 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{5B03EF4E-E0AC-4905-861B-8C3EC1A0D458}" -ProjectSection(SolutionItems) = preProject - ..\build\NuSpecs\build\Umbraco.Cms.props = ..\build\NuSpecs\build\Umbraco.Cms.props - ..\build\NuSpecs\build\Umbraco.Cms.targets = ..\build\NuSpecs\build\Umbraco.Cms.targets -EndProjectSection + ProjectSection(SolutionItems) = preProject + ..\build\NuSpecs\build\Umbraco.Cms.props = ..\build\NuSpecs\build\Umbraco.Cms.props + ..\build\NuSpecs\build\Umbraco.Cms.targets = ..\build\NuSpecs\build\Umbraco.Cms.targets + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DocTools", "DocTools", "{53594E5B-64A2-4545-8367-E3627D266AE8}" ProjectSection(SolutionItems) = preProject @@ -139,8 +139,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Core", "Umbraco.Core\Umbraco.Core.csproj", "{29AA69D9-B597-4395-8D42-43B1263C240A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder.Embedded", "Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Infrastructure", "Umbraco.Infrastructure\Umbraco.Infrastructure.csproj", "{3AE7BF57-966B-45A5-910A-954D7C554441}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Persistence.SqlCe", "Umbraco.Persistence.SqlCe\Umbraco.Persistence.SqlCe.csproj", "{33085570-9BF2-4065-A9B0-A29D920D13BA}" @@ -195,10 +193,6 @@ Global {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.Build.0 = Debug|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.ActiveCfg = Release|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.Build.0 = Release|Any CPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/umbraco.sln.DotSettings b/src/umbraco.sln.DotSettings index 2f99fe6350..6fb927035e 100644 --- a/src/umbraco.sln.DotSettings +++ b/src/umbraco.sln.DotSettings @@ -5,5 +5,6 @@ HINT False Default + True True True