diff --git a/src/Umbraco.Core/Mapping/Mapper.cs b/src/Umbraco.Core/Mapping/Mapper.cs index 8d89585bfa..a1b72807bb 100644 --- a/src/Umbraco.Core/Mapping/Mapper.cs +++ b/src/Umbraco.Core/Mapping/Mapper.cs @@ -9,6 +9,10 @@ namespace Umbraco.Core.Mapping // FIXME in order to transition, this should also handle AutoMapper? // FIXME we might have to manage a 'context' for some contextual mappings? // FIXME we have an infinite loop problem w/ logging in due to mapping issues + // FIXME refactor + // ctor: (source, context) => + // map: (source, target, context) => + // and context.Mapper is mapper public class Mapper { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 5810b3f13b..97ac224353 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -56,7 +56,7 @@ - 1.0.0 + 1.0.5 runtime; build; native; contentfiles; analyzers all diff --git a/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs b/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs index 20967f5180..a72ba41ab6 100644 --- a/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs +++ b/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs @@ -16,12 +16,18 @@ namespace Umbraco.Web.Composing.CompositionExtensions // register the profiles composition.WithCollectionBuilder() .Append() + .Append() + .Append() + .Append() .Append() - .Append(); + .Append() + .Append() + .Append() + .Append(); //register the profiles //composition.Register(); - composition.Register(); + //composition.Register(); composition.Register(); composition.Register(); composition.Register(); @@ -31,13 +37,13 @@ namespace Umbraco.Web.Composing.CompositionExtensions composition.Register(); composition.Register(); composition.Register(); - composition.Register(); - composition.Register(); + //composition.Register(); + //composition.Register(); //composition.Register(); //composition.Register(); - composition.Register(); - composition.Register(); - composition.Register(); + //composition.Register(); + //composition.Register(); + //composition.Register(); //register any resolvers, etc.. that the profiles use composition.Register(); diff --git a/src/Umbraco.Web/Models/Mapping/CodeFileMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/CodeFileMapperProfile.cs index 563935676f..9416123d0c 100644 --- a/src/Umbraco.Web/Models/Mapping/CodeFileMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/CodeFileMapperProfile.cs @@ -1,65 +1,75 @@ -using AutoMapper; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; using Stylesheet = Umbraco.Core.Models.Stylesheet; namespace Umbraco.Web.Models.Mapping { - public class CodeFileMapperProfile : Profile + public class CodeFileMapperProfile : IMapperProfile { - public CodeFileMapperProfile() + public void SetMaps(Mapper mapper) { - CreateMap() - .ForMember(dest => dest.Id, opt => opt.MapFrom(sheet => sheet.Id)) - .ForMember(dest => dest.Alias, opt => opt.MapFrom(sheet => sheet.Alias)) - .ForMember(dest => dest.Key, opt => opt.MapFrom(sheet => sheet.Key)) - .ForMember(dest => dest.Name, opt => opt.MapFrom(sheet => sheet.Name)) - .ForMember(dest => dest.ParentId, opt => opt.MapFrom(_ => -1)) - .ForMember(dest => dest.Path, opt => opt.MapFrom(sheet => sheet.Path)) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .ForMember(dest => dest.Udi, opt => opt.Ignore()) - .ForMember(dest => dest.Icon, opt => opt.Ignore()); + mapper.SetMap(source => new EntityBasic(), Map); + mapper.SetMap(source => new CodeFileDisplay(), Map); + mapper.SetMap(source => new CodeFileDisplay(), Map); + mapper.SetMap(source => new CodeFileDisplay(), Map); + mapper.SetMap(Map); + mapper.SetMap(Map); - CreateMap() - .ForMember(dest => dest.FileType, opt => opt.Ignore()) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.Snippet, opt => opt.Ignore()); + } - CreateMap() - .ForMember(dest => dest.FileType, opt => opt.Ignore()) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.Snippet, opt => opt.Ignore()); + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(Stylesheet source, EntityBasic target) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } - CreateMap() - .ForMember(dest => dest.FileType, opt => opt.Ignore()) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.Snippet, opt => opt.Ignore()); + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(IPartialView source, CodeFileDisplay target) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } - CreateMap() - .IgnoreEntityCommonProperties() - .ForMember(dest => dest.Id, opt => opt.Ignore()) - .ForMember(dest => dest.Key, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.Alias, opt => opt.Ignore()) - .ForMember(dest => dest.Name, opt => opt.Ignore()) - .ForMember(dest => dest.OriginalPath, opt => opt.Ignore()) - .ForMember(dest => dest.HasIdentity, opt => opt.Ignore()); + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(Script source, CodeFileDisplay target) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } - CreateMap() - .IgnoreEntityCommonProperties() - .ForMember(dest => dest.Id, opt => opt.Ignore()) - .ForMember(dest => dest.Key, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.Alias, opt => opt.Ignore()) - .ForMember(dest => dest.Name, opt => opt.Ignore()) - .ForMember(dest => dest.OriginalPath, opt => opt.Ignore()) - .ForMember(dest => dest.HasIdentity, opt => opt.Ignore()); + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(Stylesheet source, CodeFileDisplay target) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate + // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path + private static void Map(CodeFileDisplay source, IPartialView target) + { + target.Content = source.Content; + target.VirtualPath = source.VirtualPath; + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate -GetFileContent + // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path + private static void Map(CodeFileDisplay source, Script target) + { + target.Content = source.Content; + target.VirtualPath = source.VirtualPath; } } } diff --git a/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs index a838180622..83bf3d2491 100644 --- a/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs @@ -1,61 +1,56 @@ -using System.Collections.Generic; -using System.Globalization; +using System; +using System.Collections.Generic; using System.Linq; -using AutoMapper; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; using Language = Umbraco.Web.Models.ContentEditing.Language; namespace Umbraco.Web.Models.Mapping { - internal class LanguageMapperProfile : Profile + internal class LanguageMapperProfile : IMapperProfile { - public LanguageMapperProfile() + public void SetMaps(Mapper mapper) { - CreateMap() - .ForMember(dest => dest.Id, opt => opt.MapFrom(x => x.Id)) - .ForMember(dest => dest.Name, opt => opt.MapFrom(x => x.CultureName)) - .ForMember(dest => dest.Key, opt => opt.MapFrom(x => x.Key)) - .ForMember(dest => dest.Alias, opt => opt.MapFrom(x => x.IsoCode)) - .ForMember(dest => dest.ParentId, opt => opt.MapFrom(_ => -1)) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .ForMember(dest => dest.Udi, opt => opt.Ignore()) - .ForMember(dest => dest.Icon, opt => opt.Ignore()); - - CreateMap() - .ForMember(l => l.Name, expression => expression.MapFrom(x => x.CultureInfo.DisplayName)); - - CreateMap, IEnumerable>() - .ConvertUsing(); - + mapper.SetMap(source => new EntityBasic(), Map); + mapper.SetMap(source => new Language(), Map); + mapper.SetMap, IEnumerable>(source => new List(), (source, target) => Map(source, target, mapper)); } - /// - /// Converts a list of to a list of and ensures the correct order and defaults are set - /// - // ReSharper disable once ClassNeverInstantiated.Local - private class LanguageCollectionTypeConverter : ITypeConverter, IEnumerable> + // Umbraco.Code.MapAll -Udi -Path -Trashed -AdditionalData -Icon + private static void Map(ILanguage source, EntityBasic target) { - public IEnumerable Convert(IEnumerable source, IEnumerable destination, ResolutionContext context) - { - var langs = source.Select(x => context.Mapper.Map(x, null, context)).ToList(); + target.Name = source.CultureName; + target.Key = source.Key; + target.ParentId = -1; + target.Alias = source.IsoCode; + target.Id = source.Id; + } - //Put the default language first in the list & then sort rest by a-z - var defaultLang = langs.SingleOrDefault(x => x.IsDefault); + // Umbraco.Code.MapAll + private static void Map(ILanguage source, Language target) + { + target.Id = source.Id; + target.IsoCode = source.IsoCode; + target.Name = source.CultureInfo.DisplayName; + target.IsDefault = source.IsDefault; + target.IsMandatory = source.IsMandatory; + target.FallbackLanguageId = source.FallbackLanguageId; + } - //Remove the default language from the list for now - langs.Remove(defaultLang); + private static void Map(IEnumerable source, IEnumerable target, Mapper mapper) + { + if (!(target is List list)) + throw new NotSupportedException($"{nameof(target)} must be a List."); - //Sort the remaining languages a-z - langs = langs.OrderBy(x => x.Name).ToList(); + var temp = source.Select(mapper.Map).ToList(); - //Insert the default language as the first item - langs.Insert(0, defaultLang); + //Put the default language first in the list & then sort rest by a-z + var defaultLang = temp.SingleOrDefault(x => x.IsDefault); - return langs; - } + // insert default lang first, then remaining language a-z + list.Add(defaultLang); + list.AddRange(temp.Where(x => x != defaultLang).OrderBy(x => x.Name)); } } } diff --git a/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs index 7ce5c8927f..fabcf63163 100644 --- a/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs @@ -1,22 +1,34 @@ -using AutoMapper; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; -using Umbraco.Web.Composing; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Routing; namespace Umbraco.Web.Models.Mapping { - internal class RedirectUrlMapperProfile : Profile + internal class RedirectUrlMapperProfile : IMapperProfile { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; public RedirectUrlMapperProfile(IUmbracoContextAccessor umbracoContextAccessor) { - CreateMap() - .ForMember(x => x.OriginalUrl, expression => expression.MapFrom(item => Current.UmbracoContext.UrlProvider.GetUrlFromRoute(item.ContentId, item.Url, item.Culture))) - .ForMember(x => x.DestinationUrl, expression => expression.MapFrom(item => item.ContentId > 0 ? GetUrl(umbracoContextAccessor, item) : "#")) - .ForMember(x => x.RedirectId, expression => expression.MapFrom(item => item.Key)); + _umbracoContextAccessor = umbracoContextAccessor; } - private static string GetUrl(IUmbracoContextAccessor umbracoContextAccessor, IRedirectUrl item) => umbracoContextAccessor?.UmbracoContext?.UrlProvider?.GetUrl(item.ContentId, item.Culture); + private UmbracoContext UmbracoContext => _umbracoContextAccessor.UmbracoContext; + + public void SetMaps(Mapper mapper) + { + mapper.SetMap(source => new ContentRedirectUrl(), Map); + } + + // Umbraco.Code.MapAll + private void Map(IRedirectUrl source, ContentRedirectUrl target) + { + target.ContentId = source.ContentId; + target.CreateDateUtc = source.CreateDateUtc; + target.Culture = source.Culture; + target.DestinationUrl = source.ContentId > 0 ? UmbracoContext?.UrlProvider?.GetUrl(source.ContentId, source.Culture) : "#"; + target.OriginalUrl = UmbracoContext?.UrlProvider?.GetUrlFromRoute(source.ContentId, source.Url, source.Culture); + target.RedirectId = source.Key; + } } } diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs index e31b1877d3..3a0a19b536 100644 --- a/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs @@ -1,47 +1,56 @@ -using AutoMapper; -using Umbraco.Core; +using Umbraco.Core; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { - internal class RelationMapperProfile : Profile + internal class RelationMapperProfile : IMapperProfile { - public RelationMapperProfile() + public void SetMaps(Mapper mapper) { - // FROM IRelationType to RelationTypeDisplay - CreateMap() - .ForMember(dest => dest.Icon, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.Alias, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .ForMember(dest => dest.ChildObjectTypeName, opt => opt.Ignore()) - .ForMember(dest => dest.ParentObjectTypeName, opt => opt.Ignore()) - .ForMember(dest => dest.Relations, opt => opt.Ignore()) - .ForMember(dest => dest.ParentId, opt => opt.Ignore()) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.Udi, opt => opt.MapFrom(content => Udi.Create(Constants.UdiEntityType.RelationType, content.Key))) - .AfterMap((src, dest) => - { - // Build up the path - dest.Path = "-1," + src.Id; + mapper.SetMap(source => new RelationTypeDisplay(), Map); + mapper.SetMap(source => new RelationDisplay(), Map); + mapper.SetMap(Map); + } - // Set the "friendly" names for the parent and child object types - dest.ParentObjectTypeName = ObjectTypes.GetUmbracoObjectType(src.ParentObjectType).GetFriendlyName(); - dest.ChildObjectTypeName = ObjectTypes.GetUmbracoObjectType(src.ChildObjectType).GetFriendlyName(); - }); + // Umbraco.Code.MapAll -Icon -Trashed -Alias -AdditionalData + // Umbraco.Code.MapAll -Relations -ParentId -Notifications + private static void Map(IRelationType source, RelationTypeDisplay target) + { + target.ChildObjectType = source.ChildObjectType; + target.Id = source.Id; + target.IsBidirectional = source.IsBidirectional; + target.Key = source.Key; + target.Name = source.Name; + target.ParentObjectType = source.ParentObjectType; + target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); + target.Path = "-1," + source.Id; - // FROM IRelation to RelationDisplay - CreateMap() - .ForMember(dest => dest.ParentName, opt => opt.Ignore()) - .ForMember(dest => dest.ChildName, opt => opt.Ignore()); + // Set the "friendly" names for the parent and child object types + target.ParentObjectTypeName = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType).GetFriendlyName(); + target.ChildObjectTypeName = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType).GetFriendlyName(); + } - // FROM RelationTypeSave to IRelationType - CreateMap() - .ForMember(dest => dest.CreateDate, opt => opt.Ignore()) - .ForMember(dest => dest.UpdateDate, opt => opt.Ignore()) - .ForMember(dest => dest.DeleteDate, opt => opt.Ignore()); + // Umbraco.Code.MapAll -ParentName -ChildName + private static void Map(IRelation source, RelationDisplay target) + { + target.ChildId = source.ChildId; + target.Comment = source.Comment; + target.CreateDate = source.CreateDate; + target.ParentId = source.ParentId; + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + private static void Map(RelationTypeSave source, IRelationType target) + { + target.Alias = source.Alias; + target.ChildObjectType = source.ChildObjectType; + target.Id = source.Id.TryConvertTo().Result; + target.IsBidirectional = source.IsBidirectional; + target.Key = source.Key; + target.Name = source.Name; + target.ParentObjectType = source.ParentObjectType; } } } diff --git a/src/Umbraco.Web/Models/Mapping/SectionMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/SectionMapperProfile.cs index 91e3971d7c..47f32b63e6 100644 --- a/src/Umbraco.Web/Models/Mapping/SectionMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/SectionMapperProfile.cs @@ -39,7 +39,7 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll - private void Map(Section source, ManifestSection target) + private static void Map(Section source, ManifestSection target) { target.Alias = source.Alias; target.Name = source.Name; diff --git a/src/Umbraco.Web/Models/Mapping/TagMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/TagMapperProfile.cs index e788db8694..723b0b3fc8 100644 --- a/src/Umbraco.Web/Models/Mapping/TagMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/TagMapperProfile.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll - private void Map(ITag source, TagModel target) + private static void Map(ITag source, TagModel target) { target.Id = source.Id; target.Text = source.Text; diff --git a/src/Umbraco.Web/Models/Mapping/TemplateMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/TemplateMapperProfile.cs index 3c283cb3a5..1b4e84387c 100644 --- a/src/Umbraco.Web/Models/Mapping/TemplateMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/TemplateMapperProfile.cs @@ -1,24 +1,42 @@ -using AutoMapper; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { - internal class TemplateMapperProfile : Profile + internal class TemplateMapperProfile : IMapperProfile { - public TemplateMapperProfile() + public void SetMaps(Mapper mapper) { - CreateMap() - .ForMember(dest => dest.Notifications, opt => opt.Ignore()); + mapper.SetMap(source => new TemplateDisplay(), Map); + mapper.SetMap(source => new Template(source.Name, source.Alias), Map); + } - CreateMap() - .IgnoreEntityCommonProperties() - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.VirtualPath, opt => opt.Ignore()) - .ForMember(dest => dest.Path, opt => opt.Ignore()) - .ForMember(dest => dest.MasterTemplateId, opt => opt.Ignore()) // ok, assigned when creating the template - .ForMember(dest => dest.IsMasterTemplate, opt => opt.Ignore()) - .ForMember(dest => dest.HasIdentity, opt => opt.Ignore()); + // Umbraco.Code.MapAll + private static void Map(ITemplate source, TemplateDisplay target) + { + target.Id = source.Id; + target.Name = source.Name; + target.Alias = source.Alias; + target.Key = source.Key; + target.Content = source.Content; + target.Path = source.Path; + target.VirtualPath = source.VirtualPath; + target.MasterTemplateAlias = source.MasterTemplateAlias; + target.IsMasterTemplate = source.IsMasterTemplate; + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -Path -VirtualPath -MasterTemplateId -IsMasterTemplate + // Umbraco.Code.MapAll -GetFileContent + private static void Map(TemplateDisplay source, Template target) + { + target.MasterTemplateAlias = source.MasterTemplateAlias; + target.Name = source.Name; + target.Alias = source.Alias; + target.Content = source.Content; + target.Id = source.Id; + target.Key = source.Key; } } } diff --git a/src/Umbraco.Web/Models/Mapping/UserGroupDefaultPermissionsResolver.cs b/src/Umbraco.Web/Models/Mapping/UserGroupDefaultPermissionsResolver.cs deleted file mode 100644 index b13b5cda10..0000000000 --- a/src/Umbraco.Web/Models/Mapping/UserGroupDefaultPermissionsResolver.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.CodeAnnotations; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; -using Umbraco.Web.Actions; -using Umbraco.Web.Models.ContentEditing; - - -namespace Umbraco.Web.Models.Mapping -{ - /// - /// Converts an IUserGroup instance into a dictionary of permissions by category - /// - internal class UserGroupDefaultPermissionsResolver - { - private readonly ILocalizedTextService _textService; - private readonly ActionCollection _actions; - - public UserGroupDefaultPermissionsResolver(ILocalizedTextService textService, ActionCollection actions) - { - _actions = actions; - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - } - - public IDictionary> Resolve(IUserGroup source) - { - return _actions - .Where(x => x.CanBePermissionAssigned) - .Select(x => GetPermission(x, source)) - .GroupBy(x => x.Category) - .ToDictionary(x => x.Key, x => (IEnumerable) x.ToArray()); - } - - private Permission GetPermission(IAction action, IUserGroup source) - { - var result = new Permission(); - - result.Category = action.Category.IsNullOrWhiteSpace() - ? _textService.Localize($"actionCategories/{Constants.Conventions.PermissionCategories.OtherCategory}") - : _textService.Localize($"actionCategories/{action.Category}"); - result.Name = _textService.Localize($"actions/{action.Alias}"); - result.Description = _textService.Localize($"actionDescriptions/{action.Alias}"); - result.Icon = action.Icon; - result.Checked = source.Permissions != null && source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)); - result.PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture); - return result; - } - } -} diff --git a/src/Umbraco.Web/Models/Mapping/UserMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/UserMapperProfile.cs index 84660d2602..4ec9525c19 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapperProfile.cs @@ -1,344 +1,396 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; -using AutoMapper; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; +using Umbraco.Core.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; -using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Services; - namespace Umbraco.Web.Models.Mapping { - internal class UserMapperProfile : Profile + internal class UserMapperProfile : IMapperProfile { - private static string GetContentTypeIcon(EntitySlim entity) - => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + private readonly ISectionService _sectionService; + private readonly IEntityService _entityService; + private readonly IUserService _userService; + private readonly ILocalizedTextService _textService; + private readonly ActionCollection _actions; + private readonly AppCaches _appCaches; + private readonly IGlobalSettings _globalSettings; public UserMapperProfile(ILocalizedTextService textService, IUserService userService, IEntityService entityService, ISectionService sectionService, AppCaches appCaches, ActionCollection actions, IGlobalSettings globalSettings) { - var userGroupDefaultPermissionsResolver = new UserGroupDefaultPermissionsResolver(textService, actions); - - CreateMap() - .ConstructUsing(save => new UserGroup { CreateDate = DateTime.UtcNow }) - .IgnoreEntityCommonProperties() - .ForMember(dest => dest.Id, opt => opt.Condition(source => GetIntId(source.Id) > 0)) - .ForMember(dest => dest.Id, opt => opt.MapFrom(source => GetIntId(source.Id))) - .ForMember(dest => dest.Permissions, opt => opt.MapFrom(source => source.DefaultPermissions)) - .AfterMap((save, userGroup) => - { - userGroup.ClearAllowedSections(); - foreach (var section in save.Sections) - { - userGroup.AddAllowedSection(section); - } - }); - - //Used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! - CreateMap() - .IgnoreEntityCommonProperties() - .ForMember(dest => dest.Id, opt => opt.Condition(src => GetIntId(src.Id) > 0)) - .ForMember(detail => detail.TourData, opt => opt.Ignore()) - .ForMember(dest => dest.SessionTimeout, opt => opt.Ignore()) - .ForMember(dest => dest.EmailConfirmedDate, opt => opt.Ignore()) - .ForMember(dest => dest.InvitedDate, opt => opt.Ignore()) - .ForMember(dest => dest.SecurityStamp, opt => opt.Ignore()) - .ForMember(dest => dest.Avatar, opt => opt.Ignore()) - .ForMember(dest => dest.ProviderUserKey, opt => opt.Ignore()) - .ForMember(dest => dest.RawPasswordValue, opt => opt.Ignore()) - .ForMember(dest => dest.RawPasswordAnswerValue, opt => opt.Ignore()) - .ForMember(dest => dest.PasswordQuestion, opt => opt.Ignore()) - .ForMember(dest => dest.Comments, opt => opt.Ignore()) - .ForMember(dest => dest.IsApproved, opt => opt.Ignore()) - .ForMember(dest => dest.IsLockedOut, opt => opt.Ignore()) - .ForMember(dest => dest.LastLoginDate, opt => opt.Ignore()) - .ForMember(dest => dest.LastPasswordChangeDate, opt => opt.Ignore()) - .ForMember(dest => dest.LastLockoutDate, opt => opt.Ignore()) - .ForMember(dest => dest.FailedPasswordAttempts, opt => opt.Ignore()) - .ForMember(user => user.Language, opt => opt.MapFrom(save => save.Culture)) - .AfterMap((save, user) => - { - user.ClearGroups(); - var foundGroups = userService.GetUserGroupsByAlias(save.UserGroups.ToArray()); - foreach (var group in foundGroups) - { - user.AddGroup(group.ToReadOnlyGroup()); - } - }); - - CreateMap() - .IgnoreEntityCommonProperties() - .ForMember(dest => dest.Id, opt => opt.Ignore()) - .ForMember(detail => detail.TourData, opt => opt.Ignore()) - .ForMember(dest => dest.StartContentIds, opt => opt.Ignore()) - .ForMember(dest => dest.StartMediaIds, opt => opt.Ignore()) - .ForMember(dest => dest.Language, opt => opt.Ignore()) - .ForMember(dest => dest.Username, opt => opt.Ignore()) - .ForMember(dest => dest.PasswordQuestion, opt => opt.Ignore()) - .ForMember(dest => dest.SessionTimeout, opt => opt.Ignore()) - .ForMember(dest => dest.EmailConfirmedDate, opt => opt.Ignore()) - .ForMember(dest => dest.InvitedDate, opt => opt.Ignore()) - .ForMember(dest => dest.SecurityStamp, opt => opt.Ignore()) - .ForMember(dest => dest.Avatar, opt => opt.Ignore()) - .ForMember(dest => dest.ProviderUserKey, opt => opt.Ignore()) - .ForMember(dest => dest.RawPasswordValue, opt => opt.Ignore()) - .ForMember(dest => dest.RawPasswordAnswerValue, opt => opt.Ignore()) - .ForMember(dest => dest.Comments, opt => opt.Ignore()) - .ForMember(dest => dest.IsApproved, opt => opt.Ignore()) - .ForMember(dest => dest.IsLockedOut, opt => opt.Ignore()) - .ForMember(dest => dest.LastLoginDate, opt => opt.Ignore()) - .ForMember(dest => dest.LastPasswordChangeDate, opt => opt.Ignore()) - .ForMember(dest => dest.LastLockoutDate, opt => opt.Ignore()) - .ForMember(dest => dest.FailedPasswordAttempts, opt => opt.Ignore()) - //all invited users will not be approved, completing the invite will approve the user - .ForMember(user => user.IsApproved, opt => opt.MapFrom(_ => false)) - .AfterMap((invite, user) => - { - user.ClearGroups(); - var foundGroups = userService.GetUserGroupsByAlias(invite.UserGroups.ToArray()); - foreach (var group in foundGroups) - { - user.AddGroup(group.ToReadOnlyGroup()); - } - }); - - CreateMap() - .ForMember(dest => dest.ContentStartNode, opt => opt.Ignore()) - .ForMember(dest => dest.UserCount, opt => opt.Ignore()) - .ForMember(dest => dest.MediaStartNode, opt => opt.Ignore()) - .ForMember(dest => dest.Key, opt => opt.Ignore()) - .ForMember(dest => dest.Sections, opt => opt.Ignore()) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.Udi, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.ParentId, opt => opt.MapFrom(_ => -1)) - .ForMember(dest => dest.Path, opt => opt.MapFrom(userGroup => "-1," + userGroup.Id)) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .AfterMap((group, display) => - { - MapUserGroupBasic(sectionService, entityService, textService, group, display); - }); - - CreateMap() - .ForMember(dest => dest.ContentStartNode, opt => opt.Ignore()) - .ForMember(dest => dest.MediaStartNode, opt => opt.Ignore()) - .ForMember(dest => dest.Sections, opt => opt.Ignore()) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.Udi, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.ParentId, opt => opt.MapFrom(_ => -1)) - .ForMember(dest => dest.Path, opt => opt.MapFrom(userGroup => "-1," + userGroup.Id)) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .AfterMap((group, display) => - { - MapUserGroupBasic(sectionService, entityService, textService, group, display); - }); - - //create a map to assign a user group's default permissions to the AssignedUserGroupPermissions instance - CreateMap() - .ForMember(dest => dest.Udi, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .ForMember(dest => dest.Id, opt => opt.MapFrom(group => group.Id)) - .ForMember(dest => dest.ParentId, opt => opt.MapFrom(_ => -1)) - .ForMember(dest => dest.Path, opt => opt.MapFrom(userGroup => "-1," + userGroup.Id)) - .ForMember(dest => dest.DefaultPermissions, opt => opt.MapFrom(src => userGroupDefaultPermissionsResolver.Resolve(src))) - //these will be manually mapped and by default they are null - .ForMember(dest => dest.AssignedPermissions, opt => opt.Ignore()) - .AfterMap((group, display) => - { - if (display.Icon.IsNullOrWhiteSpace()) - { - display.Icon = "icon-users"; - } - }); - - CreateMap() - .ForMember(x => x.Udi, opt => opt.MapFrom(x => Udi.Create(ObjectTypes.GetUdiType(x.NodeObjectType), x.Key))) - .ForMember(basic => basic.Icon, opt => opt.MapFrom(entity => GetContentTypeIcon(entity))) - .ForMember(dto => dto.Trashed, opt => opt.Ignore()) - .ForMember(x => x.Alias, opt => opt.Ignore()) - .ForMember(x => x.AssignedPermissions, opt => opt.Ignore()) - .AfterMap((entity, basic) => - { - if (entity.NodeObjectType == Constants.ObjectTypes.Member && basic.Icon.IsNullOrWhiteSpace()) - { - basic.Icon = "icon-user"; - } - }); - - CreateMap() - .ForMember(dest => dest.ContentStartNode, opt => opt.Ignore()) - .ForMember(dest => dest.MediaStartNode, opt => opt.Ignore()) - .ForMember(dest => dest.Sections, opt => opt.Ignore()) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.Udi, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.ParentId, opt => opt.MapFrom(_ => -1)) - .ForMember(dest => dest.Path, opt => opt.MapFrom(userGroup => "-1," + userGroup.Id)) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .ForMember(dest => dest.Users, opt => opt.Ignore()) - .ForMember(dest => dest.DefaultPermissions, opt => opt.MapFrom(src => userGroupDefaultPermissionsResolver.Resolve(src))) - .ForMember(dest => dest.AssignedPermissions, opt => opt.Ignore()) - .AfterMap((group, display) => - { - MapUserGroupBasic(sectionService, entityService, textService, group, display); - - //Important! Currently we are never mapping to multiple UserGroupDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - var users = userService.GetAllInGroup(group.Id); - display.Users = Mapper.Map>(users); - - //Deal with assigned permissions: - - var allContentPermissions = userService.GetPermissions(@group, true) - .ToDictionary(x => x.EntityId, x => x); - - IEntitySlim[] contentEntities; - if (allContentPermissions.Keys.Count == 0) - { - contentEntities = Array.Empty(); - } - else - { - // a group can end up with way more than 2000 assigned permissions, - // so we need to break them into groups in order to avoid breaking - // the entity service due to too many Sql parameters. - - var list = new List(); - foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(2000)) - list.AddRange(entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); - contentEntities = list.ToArray(); - } - - var allAssignedPermissions = new List(); - foreach (var entity in contentEntities) - { - var contentPermissions = allContentPermissions[entity.Id]; - - var assignedContentPermissions = Mapper.Map(entity); - assignedContentPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(display.DefaultPermissions); - - //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions - //and we'll re-check it if it's one of the explicitly assigned ones - foreach (var permission in assignedContentPermissions.AssignedPermissions.SelectMany(x => x.Value)) - { - permission.Checked = false; - permission.Checked = contentPermissions.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); - } - - allAssignedPermissions.Add(assignedContentPermissions); - } - - display.AssignedPermissions = allAssignedPermissions; - }); - - //Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - CreateMap() - .ForMember(dest => dest.Avatars, opt => opt.MapFrom(user => user.GetUserAvatarUrls(appCaches.RuntimeCache))) - .ForMember(dest => dest.Username, opt => opt.MapFrom(user => user.Username)) - .ForMember(dest => dest.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?)user.LastLoginDate)) - .ForMember(dest => dest.UserGroups, opt => opt.MapFrom(user => user.Groups)) - .ForMember(detail => detail.Navigation, opt => opt.MapFrom(user => CreateUserEditorNavigation(textService))) - .ForMember( - dest => dest.CalculatedStartContentIds, - opt => opt.MapFrom(src => GetStartNodeValues( - src.CalculateContentStartNodeIds(entityService), - textService, entityService, UmbracoObjectTypes.Document, "content/contentRoot"))) - .ForMember( - dest => dest.CalculatedStartMediaIds, - opt => opt.MapFrom(src => GetStartNodeValues( - src.CalculateMediaStartNodeIds(entityService), - textService, entityService, UmbracoObjectTypes.Media, "media/mediaRoot"))) - .ForMember( - dest => dest.StartContentIds, - opt => opt.MapFrom(src => GetStartNodeValues( - src.StartContentIds.ToArray(), - textService, entityService, UmbracoObjectTypes.Document, "content/contentRoot"))) - .ForMember( - dest => dest.StartMediaIds, - opt => opt.MapFrom(src => GetStartNodeValues( - src.StartMediaIds.ToArray(), - textService, entityService, UmbracoObjectTypes.Media, "media/mediaRoot"))) - .ForMember(dest => dest.Culture, opt => opt.MapFrom(user => user.GetUserCulture(textService, globalSettings))) - .ForMember( - dest => dest.AvailableCultures, - opt => opt.MapFrom(user => textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName))) - .ForMember( - dest => dest.EmailHash, - opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().GenerateHash())) - .ForMember(dest => dest.ParentId, opt => opt.MapFrom(_ => -1)) - .ForMember(dest => dest.Path, opt => opt.MapFrom(user => "-1," + user.Id)) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.Udi, opt => opt.Ignore()) - .ForMember(dest => dest.Icon, opt => opt.Ignore()) - .ForMember(dest => dest.IsCurrentUser, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.ResetPasswordValue, opt => opt.Ignore()) - .ForMember(dest => dest.Alias, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()); - - CreateMap() - //Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost - //Alternatively, if this is annoying the back office UI would need to be updated to request the avatars for the list of users separately so it doesn't look - //like the load time is waiting. - .ForMember(detail => - detail.Avatars, - opt => opt.MapFrom(user => user.GetUserAvatarUrls(appCaches.RuntimeCache))) - .ForMember(dest => dest.Username, opt => opt.MapFrom(user => user.Username)) - .ForMember(dest => dest.UserGroups, opt => opt.MapFrom(user => user.Groups)) - .ForMember(dest => dest.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?)user.LastLoginDate)) - .ForMember(dest => dest.Culture, opt => opt.MapFrom(user => user.GetUserCulture(textService, globalSettings))) - .ForMember( - dest => dest.EmailHash, - opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) - .ForMember(dest => dest.ParentId, opt => opt.MapFrom(_ => -1)) - .ForMember(dest => dest.Path, opt => opt.MapFrom(user => "-1," + user.Id)) - .ForMember(dest => dest.Notifications, opt => opt.Ignore()) - .ForMember(dest => dest.IsCurrentUser, opt => opt.Ignore()) - .ForMember(dest => dest.Udi, opt => opt.Ignore()) - .ForMember(dest => dest.Icon, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.Alias, opt => opt.Ignore()) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) - .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()); - - CreateMap() - .ForMember(dest => dest.Avatars, opt => opt.MapFrom(user => user.GetUserAvatarUrls(appCaches.RuntimeCache))) - .ForMember(dest => dest.UserId, opt => opt.MapFrom(user => GetIntId(user.Id))) - .ForMember(dest => dest.StartContentIds, opt => opt.MapFrom(user => user.CalculateContentStartNodeIds(entityService))) - .ForMember(dest => dest.StartMediaIds, opt => opt.MapFrom(user => user.CalculateMediaStartNodeIds(entityService))) - .ForMember(dest => dest.Culture, opt => opt.MapFrom(user => user.GetUserCulture(textService, globalSettings))) - .ForMember( - dest => dest.EmailHash, - opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().GenerateHash())) - .ForMember(dest => dest.SecondsUntilTimeout, opt => opt.Ignore()) - .ForMember(dest => dest.UserGroups, opt => opt.Ignore()) - .AfterMap((user, detail) => - { - //we need to map the legacy UserType - //the best we can do here is to return the user's first user group as a IUserType object - //but we should attempt to return any group that is the built in ones first - var groups = user.Groups.ToArray(); - detail.UserGroups = user.Groups.Select(x => x.Alias).ToArray(); - - }); - - CreateMap() - .ForMember(dest => dest.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); + _sectionService = sectionService; + _entityService = entityService; + _userService = userService; + _textService = textService; + _actions = actions; + _appCaches = appCaches; + _globalSettings = globalSettings; } - private IEnumerable CreateUserEditorNavigation(ILocalizedTextService textService) + public void SetMaps(Mapper mapper) + { + mapper.SetMap(source => new UserGroup { CreateDate = DateTime.UtcNow }, Map); + mapper.SetMap(Map); + mapper.SetMap(Map); + mapper.SetMap(source => new UserGroupBasic(), (source, target) => Map(source, target, mapper)); + mapper.SetMap(source => new UserGroupBasic(), (source, target) => Map(source, target, mapper)); + mapper.SetMap(source => new AssignedUserGroupPermissions(), Map); + mapper.SetMap(source => new AssignedContentPermissions(), Map); + mapper.SetMap(source => new UserGroupDisplay(), (source, target) => Map(source, target, mapper)); + mapper.SetMap(source => new UserBasic(), (source, target) => Map(source, target, mapper)); + mapper.SetMap(source => new UserDetail(), Map); + + // used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! + mapper.SetMap(Map); + + // important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. + mapper.SetMap(source => new UserDisplay(), (source, target) => Map(source, target, mapper)); + } + + // mappers + + private static void Map(UserGroupSave source, IUserGroup target) + { + if (!(target is UserGroup ttarget)) + throw new NotSupportedException($"{nameof(target)} must be a UserGroup."); + Map(source, ttarget); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + private static void Map(UserGroupSave source, UserGroup target) + { + target.StartMediaId = source.StartMediaId; + target.StartContentId = source.StartContentId; + target.Icon = source.Icon; + target.Alias = source.Alias; + target.Name = source.Name; + target.Permissions = source.DefaultPermissions; + target.Key = source.Key; + + var id = GetIntId(source.Id); + if (id > 0) + target.Id = id; + + target.ClearAllowedSections(); + foreach (var section in source.Sections) + target.AddAllowedSection(section); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -Id -TourData -StartContentIds -StartMediaIds -Language -Username + // Umbraco.Code.MapAll -PasswordQuestion -SessionTimeout -EmailConfirmedDate -InvitedDate + // Umbraco.Code.MapAll -SecurityStamp -Avatar -ProviderUserKey -RawPasswordValue + // Umbraco.Code.MapAll -RawPasswordAnswerValue -Comments -IsApproved -IsLockedOut -LastLoginDate + // Umbraco.Code.MapAll -LastPasswordChangeDate -LastLockoutDate -FailedPasswordAttempts + private void Map(UserInvite source, IUser target) + { + target.Email = source.Email; + target.Key = source.Key; + target.Name = source.Name; + target.IsApproved = false; + + target.ClearGroups(); + var groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); + foreach (var group in groups) + target.AddGroup(group.ToReadOnlyGroup()); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -TourData -SessionTimeout -EmailConfirmedDate -InvitedDate -SecurityStamp -Avatar + // Umbraco.Code.MapAll -ProviderUserKey -RawPasswordValue -RawPasswordAnswerValue -PasswordQuestion -Comments + // Umbraco.Code.MapAll -IsApproved -IsLockedOut -LastLoginDate -LastPasswordChangeDate -LastLockoutDate + // Umbraco.Code.MapAll -FailedPasswordAttempts + private void Map(UserSave source, IUser target) + { + target.Name = source.Name; + target.StartContentIds = source.StartContentIds; + target.StartMediaIds = source.StartMediaIds; + target.Language = source.Culture; + target.Email = source.Email; + target.Key = source.Key; + target.Username = source.Username; + target.Id = source.Id; + + target.ClearGroups(); + var groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); + foreach (var group in groups) + target.AddGroup(group.ToReadOnlyGroup()); + } + + // Umbraco.Code.MapAll + private static void Map(IProfile source, ContentEditing.UserProfile target) + { + target.Name = source.Name; + target.UserId = source.Id; + } + + // Umbraco.Code.MapAll -ContentStartNode -UserCount -MediaStartNode -Key -Sections + // Umbraco.Code.MapAll -Notifications -Udi -Trashed -AdditionalData + private void Map(IReadOnlyUserGroup source, UserGroupBasic target, Mapper mapper) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, mapper); + } + + // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications + // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData + private void Map(IUserGroup source, UserGroupBasic target, Mapper mapper) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserCount = source.UserCount; + MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, mapper); + } + + // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -Id -AssignedPermissions + private void Map(IUserGroup source, AssignedUserGroupPermissions target) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + + target.DefaultPermissions = MapUserGroupDefaultPermissions(source); + + if (target.Icon.IsNullOrWhiteSpace()) + target.Icon = "icon-users"; + } + + // Umbraco.Code.MapAll -Trashed -Alias -AssignedPermissions + private static void Map(EntitySlim source, AssignedContentPermissions target) + { + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) + target.Icon = "icon-user"; + } + + // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications -Udi + // Umbraco.Code.MapAll -Trashed -AdditionalData -Users -AssignedPermissions + private void Map(IUserGroup source, UserGroupDisplay target, Mapper mapper) + { + target.Alias = source.Alias; + target.DefaultPermissions = MapUserGroupDefaultPermissions(source); + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserCount = source.UserCount; + + MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, mapper); + + //Important! Currently we are never mapping to multiple UserGroupDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. + var users = _userService.GetAllInGroup(source.Id); + target.Users = mapper.Map>(users); + + //Deal with assigned permissions: + + var allContentPermissions = _userService.GetPermissions(source, true) + .ToDictionary(x => x.EntityId, x => x); + + IEntitySlim[] contentEntities; + if (allContentPermissions.Keys.Count == 0) + { + contentEntities = Array.Empty(); + } + else + { + // a group can end up with way more than 2000 assigned permissions, + // so we need to break them into groups in order to avoid breaking + // the entity service due to too many Sql parameters. + + var list = new List(); + foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(2000)) + list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); + contentEntities = list.ToArray(); + } + + var allAssignedPermissions = new List(); + foreach (var entity in contentEntities) + { + var contentPermissions = allContentPermissions[entity.Id]; + + var assignedContentPermissions = mapper.Map(entity); + assignedContentPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(target.DefaultPermissions); + + //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions + //and we'll re-check it if it's one of the explicitly assigned ones + foreach (var permission in assignedContentPermissions.AssignedPermissions.SelectMany(x => x.Value)) + { + permission.Checked = false; + permission.Checked = contentPermissions.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); + } + + allAssignedPermissions.Add(assignedContentPermissions); + } + + target.AssignedPermissions = allAssignedPermissions; + } + + // Umbraco.Code.MapAll -Notifications -Udi -Icon -IsCurrentUser -Trashed -ResetPasswordValue + // Umbraco.Code.MapAll -Alias -AdditionalData + private void Map(IUser source, UserDisplay target, Mapper mapper) + { + target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); + target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService), UmbracoObjectTypes.Document, "content/contentRoot", mapper); + target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService), UmbracoObjectTypes.Media, "media/mediaRoot", mapper); + target.CreateDate = source.CreateDate; + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email.ToLowerInvariant().Trim().GenerateHash(); + target.FailedPasswordAttempts = source.FailedPasswordAttempts; + target.Id = source.Id; + target.Key = source.Key; + target.LastLockoutDate = source.LastLockoutDate; + target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?) source.LastLoginDate; + target.LastPasswordChangeDate = source.LastPasswordChangeDate; + target.Name = source.Name; + target.Navigation = CreateUserEditorNavigation(); + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.StartContentIds = GetStartNodes(source.StartContentIds.ToArray(), UmbracoObjectTypes.Document, "content/contentRoot", mapper); + target.StartMediaIds = GetStartNodes(source.StartMediaIds.ToArray(), UmbracoObjectTypes.Media, "media/mediaRoot", mapper); + target.UpdateDate = source.UpdateDate; + target.UserGroups = source.Groups.Select(mapper.Map); + target.Username = source.Username; + target.UserState = source.UserState; + } + + // Umbraco.Code.MapAll -Notifications -IsCurrentUser -Udi -Icon -Trashed -Alias -AdditionalData + private void Map(IUser source, UserBasic target, Mapper mapper) + { + //Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost + //Alternatively, if this is annoying the back office UI would need to be updated to request the avatars for the list of users separately so it doesn't look + //like the load time is waiting. + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email.ToLowerInvariant().Trim().ToMd5(); + target.Id = source.Id; + target.Key = source.Key; + target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?) source.LastLoginDate; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserGroups = source.Groups.Select(mapper.Map); + target.Username = source.Username; + target.UserState = source.UserState; + } + + // Umbraco.Code.MapAll -SecondsUntilTimeout + private void Map(IUser source, UserDetail target) + { + target.AllowedSections = source.AllowedSections; + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email.ToLowerInvariant().Trim().GenerateHash(); + target.Name = source.Name; + target.StartContentIds = source.CalculateContentStartNodeIds(_entityService); + target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService); + target.UserId = source.Id; + + //we need to map the legacy UserType + //the best we can do here is to return the user's first user group as a IUserType object + //but we should attempt to return any group that is the built in ones first + target.UserGroups = source.Groups.Select(x => x.Alias).ToArray(); + } + + // helpers + + private void MapUserGroupBasic(UserGroupBasic target, IEnumerable sourceAllowedSections, int? sourceStartContentId, int? sourceStartMediaId, Mapper mapper) + { + var allSections = _sectionService.GetSections(); + target.Sections = allSections.Where(x => sourceAllowedSections.Contains(x.Alias)).Select(mapper.Map
); + + if (sourceStartMediaId > 0) + target.MediaStartNode = mapper.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media)); + else if (sourceStartMediaId == -1) + target.MediaStartNode = CreateRootNode(_textService.Localize("media/mediaRoot")); + + if (sourceStartContentId > 0) + target.ContentStartNode = mapper.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document)); + else if (sourceStartContentId == -1) + target.ContentStartNode = CreateRootNode(_textService.Localize("content/contentRoot")); + + if (target.Icon.IsNullOrWhiteSpace()) + target.Icon = "icon-users"; + } + + private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) + { + Permission GetPermission(IAction action) + => new Permission + { + Category = action.Category.IsNullOrWhiteSpace() + ? _textService.Localize($"actionCategories/{Constants.Conventions.PermissionCategories.OtherCategory}") + : _textService.Localize($"actionCategories/{action.Category}"), + Name = _textService.Localize($"actions/{action.Alias}"), + Description = _textService.Localize($"actionDescriptions/{action.Alias}"), + Icon = action.Icon, + Checked = source.Permissions != null && source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)), + PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture) + }; + + return _actions + .Where(x => x.CanBePermissionAssigned) + .Select(GetPermission) + .GroupBy(x => x.Category) + .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); + } + + private static string MapContentTypeIcon(EntitySlim entity) + => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + + private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, Mapper mapper) + { + if (startNodeIds.Length <= 0) + return Enumerable.Empty(); + + var startNodes = new List(); + if (startNodeIds.Contains(-1)) + startNodes.Add(CreateRootNode(_textService.Localize(localizedKey))); + + var mediaItems = _entityService.GetAll(objectType, startNodeIds); + startNodes.AddRange(mapper.Map, IEnumerable>(mediaItems)); + return startNodes; + } + + private IEnumerable CreateUserEditorNavigation() { return new[] { @@ -347,74 +399,12 @@ namespace Umbraco.Web.Models.Mapping Active = true, Alias = "details", Icon = "icon-umb-users", - Name = textService.Localize("general/user"), + Name = _textService.Localize("general/user"), View = "views/users/views/user/details.html" } }; } - private IEnumerable GetStartNodeValues(int[] startNodeIds, - ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType, - string localizedKey) - { - if (startNodeIds.Length <= 0) - return Enumerable.Empty(); - - var startNodes = new List(); - if (startNodeIds.Contains(-1)) - startNodes.Add(RootNode(textService.Localize(localizedKey))); - - var mediaItems = entityService.GetAll(objectType, startNodeIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); - return startNodes; - } - - private void MapUserGroupBasic(ISectionService sectionService, IEntityService entityService, ILocalizedTextService textService, dynamic group, UserGroupBasic display) - { - var allSections = sectionService.GetSections(); - display.Sections = allSections.Where(x => Enumerable.Contains(group.AllowedSections, x.Alias)).Select(Mapper.Map); - - if (group.StartMediaId > 0) - { - display.MediaStartNode = Mapper.Map( - entityService.Get(group.StartMediaId, UmbracoObjectTypes.Media)); - } - else if (group.StartMediaId == -1) - { - //create the root node - display.MediaStartNode = RootNode(textService.Localize("media/mediaRoot")); - } - - if (group.StartContentId > 0) - { - display.ContentStartNode = Mapper.Map( - entityService.Get(group.StartContentId, UmbracoObjectTypes.Document)); - } - else if (group.StartContentId == -1) - { - //create the root node - display.ContentStartNode = RootNode(textService.Localize("content/contentRoot")); - } - - if (display.Icon.IsNullOrWhiteSpace()) - { - display.Icon = "icon-users"; - } - } - - private EntityBasic RootNode(string name) - { - return new EntityBasic - { - Name = name, - Path = "-1", - Icon = "icon-folder", - Id = -1, - Trashed = false, - ParentId = -1 - }; - } - private static int GetIntId(object id) { var result = id.TryConvertTo(); @@ -425,5 +415,18 @@ namespace Umbraco.Web.Models.Mapping } return result.Result; } + + private EntityBasic CreateRootNode(string name) + { + return new EntityBasic + { + Name = name, + Path = "-1", + Icon = "icon-folder", + Id = -1, + Trashed = false, + ParentId = -1 + }; + } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 3049a57f3d..cf1a98fefb 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -87,7 +87,7 @@ - 1.0.0 + 1.0.5 @@ -446,7 +446,6 @@ -