From cbb36a900a08cb5dec2df6a923aafd2a694dbf04 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 26 Oct 2016 22:38:54 +0200 Subject: [PATCH 001/714] Management of user groups with assignment of users. Migration for creation of user group tables and data amends for moving from permissions on users to those on groups. --- .../Models/Membership/EntityPermission.cs | 16 +- src/Umbraco.Core/Models/Membership/IUser.cs | 21 +- .../Models/Membership/IUserGroup.cs | 13 + .../Models/Membership/IUserType.cs | 24 +- .../Models/Membership/IUserTypeGroupBase.cs | 26 ++ src/Umbraco.Core/Models/Membership/User.cs | 41 +- .../Models/Membership/UserGroup.cs | 36 +- .../Models/Membership/UserType.cs | 62 +-- .../Models/Membership/UserTypeGroupBase.cs | 68 ++++ .../Models/Rdbms/User2UserGroupDto.cs | 19 + .../Models/Rdbms/UserGroup2AppDto.cs | 19 + .../Rdbms/UserGroup2NodePermissionDto.cs | 22 ++ src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs | 32 ++ .../Persistence/Factories/UserFactory.cs | 54 +-- .../Persistence/Factories/UserGroupFactory.cs | 77 ++++ .../Persistence/Mappers/UserGroupMapper.cs | 41 ++ .../Initial/DatabaseSchemaCreation.cs | 10 +- .../AddUserGroupTables.cs | 138 +++++++ .../Relators/UserGroupSectionRelator.cs | 50 +++ .../Interfaces/IUserGroupRepository.cs | 52 +++ .../Interfaces/IUserRepository.cs | 23 +- .../Repositories/UserGroupRepository.cs | 263 +++++++++++++ .../Repositories/UserRepository.cs | 128 +++++- .../Persistence/RepositoryFactory.cs | 8 + src/Umbraco.Core/Services/IUserService.cs | 95 ++++- src/Umbraco.Core/Services/UserService.cs | 372 +++++++++++++++++- src/Umbraco.Core/Umbraco.Core.csproj | 13 + .../Persistence/BaseTableByTableTest.cs | 126 +++--- .../Services/SectionServiceTests.cs | 102 +++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + src/Umbraco.Web.UI.Client/src/less/forms.less | 5 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 + .../config/trees.Release.config | 5 +- src/Umbraco.Web.UI/config/trees.config | 5 +- .../umbraco/config/create/UI.Release.xml | 20 +- .../umbraco/config/create/UI.xml | 8 + src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 8 +- .../umbraco/config/lang/en_us.xml | 4 + .../umbraco/users/EditUserGroup.aspx | 52 +++ src/Umbraco.Web/Security/WebSecurity.cs | 4 +- src/Umbraco.Web/Umbraco.Web.csproj | 12 + .../umbraco/Trees/UserGroups.cs | 50 +++ .../umbraco/Trees/UserTypes.cs | 8 - .../umbraco/users/EditUser.aspx.cs | 108 ++++- .../umbraco/users/EditUserGroup.aspx | 52 +++ .../umbraco/users/EditUserGroup.aspx.cs | 175 ++++++++ .../users/EditUserGroup.aspx.designer.cs | 195 +++++++++ .../umbraco/users/EditUserGroupsBase.cs | 39 ++ src/umbraco.businesslogic/User.cs | 62 +-- src/umbraco.businesslogic/UserGroup.cs | 309 +++++++++++++++ .../umbraco.businesslogic.csproj | 1 + 51 files changed, 2812 insertions(+), 263 deletions(-) create mode 100644 src/Umbraco.Core/Models/Membership/IUserGroup.cs create mode 100644 src/Umbraco.Core/Models/Membership/IUserTypeGroupBase.cs create mode 100644 src/Umbraco.Core/Models/Membership/UserTypeGroupBase.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs create mode 100644 src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs create mode 100644 src/Umbraco.Core/Persistence/Mappers/UserGroupMapper.cs create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs create mode 100644 src/Umbraco.Core/Persistence/Relators/UserGroupSectionRelator.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs create mode 100644 src/Umbraco.Tests/Services/SectionServiceTests.cs create mode 100644 src/Umbraco.Web.UI/umbraco/users/EditUserGroup.aspx create mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs create mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx create mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs create mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.designer.cs create mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroupsBase.cs create mode 100644 src/umbraco.businesslogic/UserGroup.cs diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs index 7ab1ddc817..520a9b8d37 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Linq; namespace Umbraco.Core.Models.Membership { @@ -21,6 +21,20 @@ namespace Umbraco.Core.Models.Membership /// The assigned permissions for the user/entity combo /// public string[] AssignedPermissions { get; private set; } + + /// + /// Adds additional permissions to an existing instance of + /// ensuring that only ones that aren't already assigned are added + /// + /// + public void AddAdditionalPermissions(string[] additionalPermissions) + { + var newPermissions = AssignedPermissions.ToList(); + newPermissions.AddRange(additionalPermissions); + AssignedPermissions = newPermissions + .Distinct() + .ToArray(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index f1f9c23971..095044c47e 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Models.Membership { @@ -15,12 +14,28 @@ namespace Umbraco.Core.Models.Membership int StartContentId { get; set; } int StartMediaId { get; set; } string Language { get; set; } - + /// /// Gets/sets the user type for the user /// IUserType UserType { get; set; } + /// + /// Gets the groups that user is part of + /// + IEnumerable Groups { get; } + + /// + /// Indicates if the groups for a user have been loaded + /// + bool GroupsLoaded { get; } + + void RemoveGroup(IUserGroup group); + + void AddGroup(IUserGroup group); + + void SetGroupsLoaded(); + //TODO: This should be a private set /// /// The default permission set for the user @@ -31,7 +46,9 @@ namespace Umbraco.Core.Models.Membership IEnumerable DefaultPermissions { get; set; } IEnumerable AllowedSections { get; } + void RemoveAllowedSection(string sectionAlias); + void AddAllowedSection(string sectionAlias); /// diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs new file mode 100644 index 0000000000..fc1af97912 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Membership +{ + public interface IUserGroup : IUserTypeGroupBase + { + IEnumerable AllowedSections { get; } + + void RemoveAllowedSection(string sectionAlias); + + void AddAllowedSection(string sectionAlias); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/IUserType.cs b/src/Umbraco.Core/Models/Membership/IUserType.cs index fe678afd2b..ccfdc729f5 100644 --- a/src/Umbraco.Core/Models/Membership/IUserType.cs +++ b/src/Umbraco.Core/Models/Membership/IUserType.cs @@ -1,28 +1,6 @@ -using System.Collections.Generic; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence.Mappers; - namespace Umbraco.Core.Models.Membership { - - public interface IUserType : IAggregateRoot + public interface IUserType : IUserTypeGroupBase { - /// - /// The user type alias - /// - string Alias { get; set; } - - /// - /// The user type name - /// - string Name { get; set; } - - /// - /// The set of default permissions for the user type - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - IEnumerable Permissions { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/IUserTypeGroupBase.cs b/src/Umbraco.Core/Models/Membership/IUserTypeGroupBase.cs new file mode 100644 index 0000000000..06769b1866 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/IUserTypeGroupBase.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Membership +{ + public interface IUserTypeGroupBase : IAggregateRoot + { + /// + /// The alias + /// + string Alias { get; set; } + + /// + /// The name + /// + string Name { get; set; } + + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. + /// + IEnumerable Permissions { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index cadda14508..6fad1a6f78 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -8,7 +8,6 @@ using System.Runtime.Serialization; using Umbraco.Core.Configuration; using Umbraco.Core.Models.EntityBase; - namespace Umbraco.Core.Models.Membership { /// @@ -29,6 +28,7 @@ namespace Umbraco.Core.Models.Membership _sectionCollection = new ObservableCollection(); _addedSections = new List(); _removedSections = new List(); + _groupCollection = new List(); _language = GlobalSettings.DefaultUILanguage; _sectionCollection.CollectionChanged += SectionCollectionChanged; _isApproved = true; @@ -58,6 +58,8 @@ namespace Umbraco.Core.Models.Membership private List _addedSections; private List _removedSections; private ObservableCollection _sectionCollection; + private List _groupCollection; + private bool _groupsLoaded; private int _sessionTimeout; private int _startContentId; private int _startMediaId; @@ -324,6 +326,41 @@ namespace Umbraco.Core.Models.Membership } } + /// + /// Gets or sets the groups that user is part of + /// + [DataMember] + public IEnumerable Groups + { + get { return _groupCollection; } + } + + /// + /// Indicates if the groups for a user have been loaded + /// + public bool GroupsLoaded { get { return _groupsLoaded; } } + + public void RemoveGroup(IUserGroup group) + { + if (_groupCollection.Select(x => x.Id).Contains(group.Id)) + { + _groupCollection.Remove(group); + } + } + + public void AddGroup(IUserGroup group) + { + if (_groupCollection.Select(x => x.Id).Contains(group.Id) == false) + { + _groupCollection.Add(group); + } + } + + public void SetGroupsLoaded() + { + _groupsLoaded = true; + } + #endregion /// @@ -365,7 +402,6 @@ namespace Umbraco.Core.Models.Membership { _removedSections.Add(item); } - } } @@ -378,6 +414,7 @@ namespace Umbraco.Core.Models.Membership clone._addedSections = new List(); clone._removedSections = new List(); clone._sectionCollection = new ObservableCollection(_sectionCollection.ToList()); + clone._groupCollection = new List(_groupCollection.ToList()); clone._defaultPermissions = new List(_defaultPermissions.ToList()); //re-create the event handler clone._sectionCollection.CollectionChanged += clone.SectionCollectionChanged; diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index b0f10d9ff1..403e40c8df 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -1,20 +1,42 @@ using System; +using System.Collections.Generic; using System.Runtime.Serialization; -using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models.Membership { /// /// Represents a Group for a Backoffice User /// - /// - /// Should be internal until a proper user/membership implementation - /// is part of the roadmap. - /// [Serializable] [DataContract(IsReference = true)] - internal class UserGroup : Entity + internal class UserGroup : UserTypeGroupBase, IUserGroup { - //Add UserCollection ? + private List _sectionCollection; + + public UserGroup() + { + _sectionCollection = new List(); + } + + public IEnumerable AllowedSections + { + get { return _sectionCollection; } + } + + public void RemoveAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias)) + { + _sectionCollection.Remove(sectionAlias); + } + } + + public void AddAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias) == false) + { + _sectionCollection.Add(sectionAlias); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/UserType.cs b/src/Umbraco.Core/Models/Membership/UserType.cs index 01183e9d9d..92e0032bd6 100644 --- a/src/Umbraco.Core/Models/Membership/UserType.cs +++ b/src/Umbraco.Core/Models/Membership/UserType.cs @@ -1,72 +1,14 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using System.Runtime.Serialization; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Strings; namespace Umbraco.Core.Models.Membership { /// - /// Represents the Type for a Backoffice User + /// Represents the Type for a back-office User /// [Serializable] [DataContract(IsReference = true)] - internal class UserType : Entity, IUserType + internal class UserType : UserTypeGroupBase, IUserType { - private string _alias; - private string _name; - private IEnumerable _permissions; - - private static readonly Lazy Ps = new Lazy(); - - private class PropertySelectors - { - public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); - public readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); - public readonly PropertyInfo PermissionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Permissions); - } - - [DataMember] - public string Alias - { - get { return _alias; } - set - { - SetPropertyValueAndDetectChanges( - value.ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase), - ref _alias, - Ps.Value.AliasSelector); - } - } - - [DataMember] - public string Name - { - get { return _name; } - set { SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); } - } - - /// - /// The set of default permissions for the user type - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - [DataMember] - public IEnumerable Permissions - { - get { return _permissions; } - set - { - SetPropertyValueAndDetectChanges(value, ref _permissions, Ps.Value.PermissionsSelector, - //Custom comparer for enumerable - new DelegateEqualityComparer>( - (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), - enum1 => enum1.GetHashCode())); - } - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/UserTypeGroupBase.cs b/src/Umbraco.Core/Models/Membership/UserTypeGroupBase.cs new file mode 100644 index 0000000000..b858747eb1 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserTypeGroupBase.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Strings; + +namespace Umbraco.Core.Models.Membership +{ + /// + /// Base class representing common fields for back-office user types and groups + /// + internal abstract class UserTypeGroupBase : Entity + { + private string _alias; + private string _name; + private IEnumerable _permissions; + + private static readonly Lazy Ps = new Lazy(); + + private class PropertySelectors + { + public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); + public readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); + public readonly PropertyInfo PermissionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Permissions); + } + + [DataMember] + public string Alias + { + get { return _alias; } + set + { + SetPropertyValueAndDetectChanges( + value.ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase), + ref _alias, + Ps.Value.AliasSelector); + } + } + + [DataMember] + public string Name + { + get { return _name; } + set { SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); } + } + + /// + /// The set of default permissions for the user type + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. + /// + [DataMember] + public IEnumerable Permissions + { + get { return _permissions; } + set + { + SetPropertyValueAndDetectChanges(value, ref _permissions, Ps.Value.PermissionsSelector, + //Custom comparer for enumerable + new DelegateEqualityComparer>( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode())); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs new file mode 100644 index 0000000000..afdbf661b5 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs @@ -0,0 +1,19 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUser2UserGroup")] + [ExplicitColumns] + internal class User2UserGroupDto + { + [Column("userId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2userGroup", OnColumns = "userId, userGroupId")] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("userGroupId")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs new file mode 100644 index 0000000000..14fc60e197 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs @@ -0,0 +1,19 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUserGroup2App")] + [ExplicitColumns] + internal class UserGroup2AppDto + { + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2App", OnColumns = "userGroupId, app")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } + + [Column("app")] + [Length(50)] + public string AppAlias { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs new file mode 100644 index 0000000000..657aad2f31 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs @@ -0,0 +1,22 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUserGroup2NodePermission")] + [ExplicitColumns] + internal class UserGroup2NodePermissionDto + { + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUserGroup2NodePermission", OnColumns = "userGroupId, nodeId, permission")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } + + [Column("permission")] + public string Permission { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs new file mode 100644 index 0000000000..dd1023b68a --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUserGroup")] + [PrimaryKey("id")] + [ExplicitColumns] + internal class UserGroupDto + { + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 5)] + public int Id { get; set; } + + [Column("userGroupAlias")] + [Length(200)] + public string Alias { get; set; } + + [Column("userGroupName")] + [Length(200)] + public string Name { get; set; } + + [Column("userGroupDefaultPermissions")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string DefaultPermissions { get; set; } + + [ResultColumn] + public List UserGroup2AppDtos { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 0a13c82447..322e8b5b65 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; @@ -44,9 +43,12 @@ namespace Umbraco.Core.Persistence.Factories user.LastLoginDate = dto.LastLoginDate ?? DateTime.MinValue; user.LastPasswordChangeDate = dto.LastPasswordChangeDate ?? DateTime.MinValue; - foreach (var app in dto.User2AppDtos) + if (dto.User2AppDtos != null) { - user.AddAllowedSection(app.AppAlias); + foreach (var app in dto.User2AppDtos) + { + user.AddAllowedSection(app.AppAlias); + } } //on initial construction we don't want to have dirty properties tracked @@ -64,41 +66,43 @@ namespace Umbraco.Core.Persistence.Factories public UserDto BuildDto(IUser entity) { var dto = new UserDto - { - ContentStartId = entity.StartContentId, - MediaStartId = entity.StartMediaId, - Disabled = entity.IsApproved == false, - Email = entity.Email, - Login = entity.Username, - NoConsole = entity.IsLockedOut, - Password = entity.RawPasswordValue, - UserLanguage = entity.Language, - UserName = entity.Name, - Type = short.Parse(entity.UserType.Id.ToString(CultureInfo.InvariantCulture)), - User2AppDtos = new List(), - SecurityStampToken = entity.SecurityStamp, - FailedLoginAttempts = entity.FailedPasswordAttempts, - LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? (DateTime?)null : entity.LastLockoutDate, - LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? (DateTime?)null : entity.LastLoginDate, - LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, - }; + { + ContentStartId = entity.StartContentId, + MediaStartId = entity.StartMediaId, + Disabled = entity.IsApproved == false, + Email = entity.Email, + Login = entity.Username, + NoConsole = entity.IsLockedOut, + Password = entity.RawPasswordValue, + UserLanguage = entity.Language, + UserName = entity.Name, + Type = short.Parse(entity.UserType.Id.ToString(CultureInfo.InvariantCulture)), + User2AppDtos = new List(), + SecurityStampToken = entity.SecurityStamp, + FailedLoginAttempts = entity.FailedPasswordAttempts, + LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? (DateTime?)null : entity.LastLockoutDate, + LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? (DateTime?)null : entity.LastLoginDate, + LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, + }; foreach (var app in entity.AllowedSections) { var appDto = new User2AppDto - { - AppAlias = app - }; + { + AppAlias = app + }; if (entity.HasIdentity) { - appDto.UserId = (int) entity.Id; + appDto.UserId = entity.Id; } dto.User2AppDtos.Add(appDto); } if (entity.HasIdentity) + { dto.Id = entity.Id.SafeCast(); + } return dto; } diff --git a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs new file mode 100644 index 0000000000..39a4d13f7f --- /dev/null +++ b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Factories +{ + internal class UserGroupFactory + { + #region Implementation of IEntityFactory + + public IUserGroup BuildEntity(UserGroupDto dto) + { + var userGroup = new UserGroup(); + + try + { + userGroup.DisableChangeTracking(); + + userGroup.Alias = dto.Alias; + userGroup.Id = dto.Id; + userGroup.Name = dto.Name; + userGroup.Permissions = dto.DefaultPermissions.IsNullOrWhiteSpace() + ? Enumerable.Empty() + : dto.DefaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); + + if (dto.UserGroup2AppDtos != null) + { + foreach (var app in dto.UserGroup2AppDtos) + { + userGroup.AddAllowedSection(app.AppAlias); + } + } + + userGroup.ResetDirtyProperties(false); + return userGroup; + } + finally + { + userGroup.EnableChangeTracking(); + } + } + + public UserGroupDto BuildDto(IUserGroup entity) + { + var dto = new UserGroupDto + { + Alias = entity.Alias, + DefaultPermissions = entity.Permissions == null ? "" : string.Join("", entity.Permissions), + Name = entity.Name, + UserGroup2AppDtos = new List(), + }; + + foreach (var app in entity.AllowedSections) + { + var appDto = new UserGroup2AppDto + { + AppAlias = app + }; + if (entity.HasIdentity) + { + appDto.UserGroupId = entity.Id; + } + + dto.UserGroup2AppDtos.Add(appDto); + } + + if (entity.HasIdentity) + dto.Id = short.Parse(entity.Id.ToString()); + + return dto; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Mappers/UserGroupMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UserGroupMapper.cs new file mode 100644 index 0000000000..69386063db --- /dev/null +++ b/src/Umbraco.Core/Persistence/Mappers/UserGroupMapper.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Mappers +{ + /// + /// Represents a to DTO mapper used to translate the properties of the public api + /// implementation to that of the database's DTO as sql: [tableName].[columnName]. + /// + [MapperFor(typeof(IUserGroup))] + [MapperFor(typeof(UserGroup))] + public sealed class UserGroupMapper : BaseMapper + { + private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); + + //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it + // otherwise that would fail because there is no public constructor. + public UserGroupMapper() + { + BuildMap(); + } + + #region Overrides of BaseMapper + + internal override ConcurrentDictionary PropertyInfoCache + { + get { return PropertyInfoCacheInstance; } + } + + internal override void BuildMap() + { + CacheMap(src => src.Id, dto => dto.Id); + CacheMap(src => src.Alias, dto => dto.Alias); + CacheMap(src => src.Name, dto => dto.Name); + CacheMap(src => src.Permissions, dto => dto.DefaultPermissions); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 423c847c47..6c06d1f06c 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -73,9 +73,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {34, typeof (TaskDto)}, {35, typeof (ContentType2ContentTypeDto)}, {36, typeof (ContentTypeAllowedContentTypeDto)}, - {37, typeof (User2AppDto)}, + // Removed in 7.6 {37, typeof (User2AppDto)}, {38, typeof (User2NodeNotifyDto)}, - {39, typeof (User2NodePermissionDto)}, + // Removed in 7.6 {39, typeof (User2NodePermissionDto)}, {40, typeof (ServerRegistrationDto)}, {41, typeof (AccessDto)}, {42, typeof (AccessRuleDto)}, @@ -84,7 +84,11 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {45, typeof (MigrationDto)}, {46, typeof (UmbracoDeployChecksumDto)}, {47, typeof (UmbracoDeployDependencyDto)}, - {48, typeof (RedirectUrlDto) } + {48, typeof (RedirectUrlDto) }, + {49, typeof (UserGroupDto) }, + {50, typeof (User2UserGroupDto) }, + {51, typeof (UserGroup2NodePermissionDto) }, + {52, typeof (UserGroup2AppDto) }, }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs new file mode 100644 index 0000000000..ac5a121b9c --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs @@ -0,0 +1,138 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; +using System.Data; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 100, GlobalSettings.UmbracoMigrationName)] + public class AddUserGroupTables : MigrationBase + { + public AddUserGroupTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + + AddNewTables(tables); + MigratePermissionsData(); + DeleteOldTables(tables); + } + + private void AddNewTables(string[] tables) + { + if (tables.InvariantContains("umbracoUserGroup") == false) + { + Create.Table("umbracoUserGroup") + .WithColumn("id").AsInt32().Identity().PrimaryKey("PK_umbracoUserGroup") + .WithColumn("userGroupAlias").AsString(200).NotNullable() + .WithColumn("userGroupName").AsString(200).NotNullable() + .WithColumn("userGroupDefaultPermissions").AsString(50).Nullable(); + } + + if (tables.InvariantContains("umbracoUser2UserGroup") == false) + { + Create.Table("umbracoUser2UserGroup") + .WithColumn("userId").AsInt32().NotNullable() + .WithColumn("userGroupId").AsInt32().NotNullable(); + Create.PrimaryKey("PK_user2userGroup") + .OnTable("umbracoUser2UserGroup") + .Columns(new[] {"userId", "userGroupId"}); + Create.ForeignKey("FK_umbracoUser2UserGroup_userId") + .FromTable("umbracoUser2UserGroup").ForeignColumn("userId") + .ToTable("umbracoUser").PrimaryColumn("id").OnDeleteOrUpdate(Rule.None); + Create.ForeignKey("FK_umbracoUser2UserGroup_userGroupId") + .FromTable("umbracoUser2UserGroup").ForeignColumn("userGroupId") + .ToTable("umbracoUserGroup").PrimaryColumn("id").OnDeleteOrUpdate(Rule.None); + } + + if (tables.InvariantContains("umbracoUserGroup2App") == false) + { + Create.Table("umbracoUserGroup2App") + .WithColumn("userGroupId").AsInt32().NotNullable() + .WithColumn("app").AsString(50).NotNullable(); + Create.PrimaryKey("PK_userGroup2App") + .OnTable("umbracoUserGroup2App") + .Columns(new[] {"userGroupId", "app"}); + Create.ForeignKey("FK_umbracoUserGroup2App_umbracoGroupUser_id") + .FromTable("umbracoUserGroup2App").ForeignColumn("userGroupId") + .ToTable("umbracoUserGroup").PrimaryColumn("id").OnDeleteOrUpdate(Rule.None); + } + + if (tables.InvariantContains("umbracoUserGroup2NodePermission") == false) + { + Create.Table("umbracoUserGroup2NodePermission") + .WithColumn("userGroupId").AsInt32().NotNullable() + .WithColumn("nodeId").AsInt32().NotNullable() + .WithColumn("permission").AsString(10).NotNullable(); + Create.PrimaryKey("PK_umbracoUserGroup2NodePermission") + .OnTable("umbracoUserGroup2NodePermission") + .Columns(new[] {"userGroupId", "nodeId", "permission"}); + Create.ForeignKey("FK_umbracoUserGroup2NodePermission_umbracoNode_id") + .FromTable("umbracoUserGroup2NodePermission").ForeignColumn("nodeId") + .ToTable("umbracoNode").PrimaryColumn("id").OnDeleteOrUpdate(Rule.None); + Create.ForeignKey("FK_umbracoUserGroup2NodePermission_umbracoUserGroup_id") + .FromTable("umbracoUserGroup2NodePermission").ForeignColumn("userGroupId") + .ToTable("umbracoUserGroup").PrimaryColumn("id").OnDeleteOrUpdate(Rule.None); + } + } + + private void MigratePermissionsData() + { + // TODO: review for MySQL and CE (tested only on SQL Express) + + // Create user group records for all non-admin users that have specific permissions set + Execute.Sql(@"INSERT INTO umbracoUserGroup(userGroupAlias, userGroupName) + SELECT userName + 'Group', 'Group for ' + userName + FROM umbracoUser + WHERE (id IN ( + SELECT [user] + FROM umbracoUser2app + ) OR id IN ( + SELECT userid + FROM umbracoUser2NodePermission + )) + AND id > 0"); + + // Associate those groups with the users + Execute.Sql(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) + SELECT (SELECT id from umbracoUser WHERE userName + 'Group' = umbracoUserGroup.userGroupAlias), id + FROM umbracoUserGroup"); + + // Create node permissions on the groups + Execute.Sql(@"INSERT INTO umbracoUserGroup2NodePermission (userGroupId,nodeId,permission) + SELECT ug.id, nodeId, permission + FROM umbracoUserGroup ug + INNER JOIN umbracoUser2UserGroup u2ug ON u2ug.userGroupId = ug.id + INNER JOIN umbracoUser u ON u.id = u2ug.userId + INNER JOIN umbracoUser2NodePermission u2np ON u2np.userId = u.id"); + + // Create app permissions on the groups + Execute.Sql(@"INSERT INTO umbracoUserGroup2app (userGroupId,app) + SELECT ug.id, app + FROM umbracoUserGroup ug + INNER JOIN umbracoUser2UserGroup u2ug ON u2ug.userGroupId = ug.id + INNER JOIN umbracoUser u ON u.id = u2ug.userId + INNER JOIN umbracoUser2app u2a ON u2a.[user] = u.id"); + } + + private void DeleteOldTables(string[] tables) + { + if (tables.InvariantContains("umbracoUser2App")) + { + Delete.Table("umbracoUser2App"); + } + + if (tables.InvariantContains("umbracoUser2NodePermission")) + { + Delete.Table("umbracoUser2NodePermission"); + } + } + + public override void Down() + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Relators/UserGroupSectionRelator.cs b/src/Umbraco.Core/Persistence/Relators/UserGroupSectionRelator.cs new file mode 100644 index 0000000000..03c5bcd398 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Relators/UserGroupSectionRelator.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Relators +{ + internal class UserGroupSectionRelator + { + internal UserGroupDto Current; + + internal UserGroupDto Map(UserGroupDto a, UserGroup2AppDto p) + { + // Terminating call. Since we can return null from this function + // we need to be ready for PetaPoco to callback later with null + // parameters + if (a == null) + return Current; + + // Is this the same DictionaryItem as the current one we're processing + if (Current != null && Current.Id == a.Id) + { + if (p.AppAlias.IsNullOrWhiteSpace() == false) + { + // Yes, just add this UserGroup2AppDto to the current item's collection + Current.UserGroup2AppDtos.Add(p); + } + + // Return null to indicate we're not done with this User yet + return null; + } + + // This is a different user group to the current one, or this is the + // first time through and we don't have one yet + + // Save the current user group + var prev = Current; + + // Setup the new current user group + Current = a; + Current.UserGroup2AppDtos = new List(); + //this can be null since we are doing a left join + if (p.AppAlias.IsNullOrWhiteSpace() == false) + { + Current.UserGroup2AppDtos.Add(p); + } + + // Return the now populated previous user group (or null if first time through) + return prev; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs new file mode 100644 index 0000000000..e50d265228 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IUserGroupRepository : IRepositoryQueryable + { + /// + /// This is useful when an entire section is removed from config + /// + /// + IEnumerable GetGroupsAssignedToSection(string sectionAlias); + + /// + /// Removes all users from a group + /// + /// Id of group + void RemoveAllUsersFromGroup(int groupId); + + /// + /// Adds a set of users to a group + /// + /// Id of group + /// Ids of users + void AddUsersToGroup(int groupId, int[] userIds); + + /* + /// + /// Gets the group permissions for the specified entities + /// + /// Id of group + /// Array of entity Ids + IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds); + + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. + void ReplaceGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds); + + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for + void AssignGroupPermission(int groupId, char permission, params int[] entityIds); + */ + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs index 542cceac40..5645cafa2a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs @@ -28,6 +28,25 @@ namespace Umbraco.Core.Persistence.Repositories /// IEnumerable GetUsersAssignedToSection(string sectionAlias); + /// + /// Gets all groups for a given user + /// + /// Id of user + /// An enumerable list of + IEnumerable GetGroupsForUser(int userId); + + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + IEnumerable GetAllInGroup(int groupId); + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + IEnumerable GetAllNotInGroup(int groupId); + /// /// Gets paged member results /// @@ -38,8 +57,8 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, Expression> orderBy); - - + + /// /// Gets the user permissions for the specified entities /// diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs new file mode 100644 index 0000000000..ba47270379 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Relators; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents the UserGroupRepository for doing CRUD operations for + /// + internal class UserGroupRepository : PetaPocoRepositoryBase, IUserGroupRepository + { + private readonly CacheHelper _cacheHelper; + + public UserGroupRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cacheHelper, logger, sqlSyntax) + { + _cacheHelper = cacheHelper; + } + + public IEnumerable GetGroupsAssignedToSection(string sectionAlias) + { + //Here we're building up a query that looks like this, a sub query is required because the resulting structure + // needs to still contain all of the section rows per user group. + + //SELECT * + //FROM [umbracoUserGroup] + //LEFT JOIN [umbracoUserGroup2App] + //ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] + //WHERE umbracoUserGroup.id IN (SELECT umbracoUserGroup.id + // FROM [umbracoUserGroup] + // LEFT JOIN [umbracoUserGroup2App] + // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] + // WHERE umbracoUserGroup2App.app = 'content') + + var sql = GetBaseQuery(false); + var innerSql = GetBaseQuery("umbracoUserGroup.id"); + innerSql.Where("umbracoUserGroup2App.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); + sql.Where(string.Format("umbracoUserGroup.id IN ({0})", innerSql.SQL)); + + return ConvertFromDtos(Database.Fetch(new UserGroupSectionRelator().Map, sql)); + } + + /// + /// Removes all users from a group + /// + /// Id of group + public void RemoveAllUsersFromGroup(int groupId) + { + Database.Delete("WHERE userGroupId = @GroupId", new { GroupId = groupId }); + } + + /// + /// Adds a set of users to a group + /// + /// Id of group + /// Ids of users + public void AddUsersToGroup(int groupId, int[] userIds) + { + foreach (var userId in userIds) + { + var dto = new User2UserGroupDto + { + UserGroupId = groupId, + UserId = userId, + }; + Database.Insert(dto); + } + } + + /* + /// + /// Gets the group permissions for the specified entities + /// + /// Id of group + /// Array of entity Ids + public IEnumerable GetGroupPermissionsForEntities(int groupId, params int[] entityIds) + { + var repo = new GroupPermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + return repo.GetGroupPermissionsForEntities(groupId, entityIds); + } + + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. + public void ReplaceGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds) + { + var repo = new GroupPermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + repo.ReplaceGroupPermissions(groupId, permissions, entityIds); + } + + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for + public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) + { + var repo = new GroupPermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + repo.AssignGroupPermission(groupId, permission, entityIds); + } + */ + + #region Overrides of RepositoryBase + + protected override IUserGroup PerformGet(int id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + + var dto = Database.Fetch(new UserGroupSectionRelator().Map, sql).FirstOrDefault(); + + if (dto == null) + return null; + + var userGroupFactory = new UserGroupFactory(); + var userGroup = userGroupFactory.BuildEntity(dto); + return userGroup; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false); + + if (ids.Any()) + { + sql.Where("umbracoUserGroup.id in (@ids)", new { ids = ids }); + } + else + { + sql.Where(x => x.Id >= 0); + } + + var dtos = Database.Fetch(new UserGroupSectionRelator().Map, sql); + return ConvertFromDtos(dtos); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var userGroupFactory = new UserGroupFactory(); + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + + var dtos = Database.Fetch(new UserGroupSectionRelator().Map, sql); + return ConvertFromDtos(dtos); + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + if (isCount) + { + sql.Select("COUNT(*)").From(); + } + else + { + return GetBaseQuery("*"); + } + return sql; + } + + protected Sql GetBaseQuery(string columns) + { + var sql = new Sql(); + sql.Select(columns) + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId); + + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoUserGroup.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @Id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @Id", + "DELETE FROM umbracoUserGroup WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistNewItem(IUserGroup entity) + { + var userGroupFactory = new UserGroupFactory(); + var userGroupDto = userGroupFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(userGroupDto)); + entity.Id = id; + + PersistAllowedSections(entity); + } + + protected override void PersistUpdatedItem(IUserGroup entity) + { + var userGroupFactory = new UserGroupFactory(); + var userGroupDto = userGroupFactory.BuildDto(entity); + + Database.Update(userGroupDto); + + PersistAllowedSections(entity); + } + + private void PersistAllowedSections(IUserGroup entity) + { + var userGroup = (UserGroup)entity; + + // First delete all + Database.Delete("WHERE UserGroupId = @UserGroupId", + new { UserGroupId = userGroup.Id }); + + // Then re-add any associated with the group + foreach (var app in userGroup.AllowedSections) + { + var dto = new UserGroup2AppDto + { + UserGroupId = userGroup.Id, + AppAlias = app + }; + Database.Insert(dto); + } + } + + #endregion + + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + return dtos.Select(dto => + { + var userGroupFactory = new UserGroupFactory(); + return userGroupFactory.BuildEntity(dto); + }); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 4652ce47be..72d20ff660 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -48,10 +48,23 @@ namespace Umbraco.Core.Persistence.Repositories var userType = _userTypeRepository.Get(dto.Type); var userFactory = new UserFactory(userType); var user = userFactory.BuildEntity(dto); - + AssociateGroupsWithUser(user); return user; } + private void AssociateGroupsWithUser(IUser user) + { + if (user != null) + { + foreach (var group in GetGroupsForUser(user.Id)) + { + user.AddGroup(group); + } + + user.SetGroupsLoaded(); + } + } + protected override IEnumerable PerformGetAll(params int[] ids) { var sql = GetBaseQuery(false); @@ -62,7 +75,7 @@ namespace Umbraco.Core.Persistence.Repositories } return ConvertFromDtos(Database.Fetch(new UserSectionRelator().Map, sql)) - .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. + .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -74,8 +87,16 @@ namespace Umbraco.Core.Persistence.Repositories var dtos = Database.Fetch(new UserSectionRelator().Map, sql) .DistinctBy(x => x.Id); - return ConvertFromDtos(dtos) - .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. + var users = ConvertFromDtos(dtos) + .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. + + // If a single user found (most likely from a look-up by an alternate key like email or username) then populate the groups + if (users.Length == 1) + { + AssociateGroupsWithUser(users[0]); + } + + return users; } #endregion @@ -100,9 +121,9 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = new Sql(); sql.Select(columns) - .From() - .LeftJoin() - .On(left => left.Id, right => right.UserId); + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserId); return sql; } @@ -225,9 +246,9 @@ namespace Umbraco.Core.Persistence.Repositories //now we need to delete any applications that have been removed foreach (var section in user.RemovedSections) { - //we need to manually delete thsi record because it has a composite key - Database.Delete("WHERE app=@Section AND " + SqlSyntax.GetQuotedColumnName("user") + "=@UserId", - new { Section = section, UserId = (int)user.Id }); + //we need to manually delete this record because it has a composite key + Database.Delete("WHERE app = @Section AND " + SqlSyntax.GetQuotedColumnName("user") + "= @UserId", + new { Section = section, UserId = user.Id }); } //for any that exist on the object, we need to determine if we need to update or insert @@ -248,7 +269,21 @@ namespace Umbraco.Core.Persistence.Repositories } } - + //update the groups + //first delete all + Database.Delete("WHERE UserId = @UserId", + new { UserId = user.Id }); + + //then re-add any associated with the user + foreach (var group in user.Groups) + { + var dto = new User2UserGroupDto + { + UserGroupId = group.Id, + UserId = user.Id + }; + Database.Insert(dto); + } } entity.ResetDirtyProperties(); @@ -304,6 +339,68 @@ namespace Umbraco.Core.Persistence.Repositories return ConvertFromDtos(Database.Fetch(new UserSectionRelator().Map, sql)); } + /// + /// Gets all groups for a given user + /// + /// Id of user + /// An enumerable list of + public IEnumerable GetGroupsForUser(int userId) + { + var sql = new Sql(); + sql.Select("*") + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId); + + var innerSql = new Sql(); + innerSql.Select("umbracoUserGroup.id") + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId) + .Where("umbracoUser2UserGroup.userId = " + userId); + + sql.Where(string.Format("umbracoUserGroup.id IN ({0})", innerSql.SQL)); + var dtos = Database.Fetch(new UserGroupSectionRelator().Map, sql); + return ConvertFromDtos(dtos); + } + + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + public IEnumerable GetAllInGroup(int groupId) + { + return GetAllInOrNotInGroup(groupId, true); + } + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + public IEnumerable GetAllNotInGroup(int groupId) + { + return GetAllInOrNotInGroup(groupId, false); + } + + private IEnumerable GetAllInOrNotInGroup(int groupId, bool include) + { + var sql = new Sql(); + sql.Select("*") + .From(); + + var innerSql = new Sql(); + innerSql.Select("umbracoUser.id") + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .Where("umbracoUser2UserGroup.userGroupId = " + groupId); + + sql.Where(string.Format("umbracoUser.id {0} ({1})", + include ? "IN" : "NOT IN", + innerSql.SQL)); + return ConvertFromDtos(Database.Fetch(sql)); + } + /// /// Gets paged user results /// @@ -418,6 +515,15 @@ namespace Umbraco.Core.Persistence.Repositories }); } + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + return dtos.Select(dto => + { + var userGroupFactory = new UserGroupFactory(); + return userGroupFactory.BuildEntity(dto); + }); + } + /// /// Dispose disposable properties /// diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index 85eb00ea87..b422dda40d 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -265,6 +265,14 @@ namespace Umbraco.Core.Persistence _logger, _sqlSyntax); } + public virtual IUserGroupRepository CreateUserGroupRepository(IDatabaseUnitOfWork uow) + { + return new UserGroupRepository( + uow, + _cacheHelper, + _logger, _sqlSyntax); + } + public virtual IUserRepository CreateUserRepository(IDatabaseUnitOfWork uow) { return new UserRepository( diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 7353f44cb8..2a3ebf9828 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -71,6 +71,30 @@ namespace Umbraco.Core.Services /// An enumerable list of IEnumerable GetPermissions(IUser user, params int[] nodeIds); + /// + /// Get permissions set for a user group and optional node ids + /// + /// Group to retrieve permissions for + /// Specifiying nothing will return all group permissions for all nodes + /// An enumerable list of + IEnumerable GetPermissions(IUserGroup group, params int[] nodeIds); + + /// + /// Gets the permissions for the provided user and path + /// + /// User to check permissions for + /// Path to check permissions for + /// String indicating permissions for provided user and path + string GetPermissionsForPath(IUser user, string path); + + /// + /// Gets the permissions for the provided group and path + /// + /// User to check permissions for + /// Path to check permissions for + /// String indicating permissions for provided user and path + string GetPermissionsForPath(IUserGroup group, string path); + /// /// Replaces the same permission set for a single user to any number of entities /// @@ -91,10 +115,24 @@ namespace Umbraco.Core.Services /// Specify the nodes to replace permissions for void AssignUserPermission(int userId, char permission, params int[] entityIds); + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + /// + IEnumerable GetAllInGroup(int groupId); + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + /// + IEnumerable GetAllNotInGroup(int groupId); + #region User types /// - /// Gets all UserTypes or thosed specified as parameters + /// Gets all UserTypes or those specified as parameters /// /// Optional Ids of UserTypes to retrieve /// An enumerable list of @@ -136,5 +174,60 @@ namespace Umbraco.Core.Services void DeleteUserType(IUserType userType); #endregion + + #region User groups + + /// + /// Gets all UserGroups or those specified as parameters + /// + /// Optional Ids of UserGroups to retrieve + /// An enumerable list of + IEnumerable GetAllUserGroups(params int[] ids); + + /// + /// Gets all UserGroups for a given user + /// + /// Id of user + /// An enumerable list of + IEnumerable GetGroupsForUser(int userId); + + /// + /// Gets a UserGroup by its Alias + /// + /// Alias of the UserGroup to retrieve + /// + IUserGroup GetUserGroupByAlias(string alias); + + /// + /// Gets a UserGroup by its Id + /// + /// Id of the UserGroup to retrieve + /// + IUserGroup GetUserGroupById(int id); + + /// + /// Gets a UserGroup by its Name + /// + /// Name of the UserGroup to retrieve + /// + IUserGroup GetUserGroupByName(string name); + + /// + /// Saves a UserGroup + /// + /// UserGroup to save + /// Flag for whether to update the list of users in the group + /// List of user Ids + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events + void SaveUserGroup(IUserGroup userGroup, bool updateUsers = false, int[] userIds = null, bool raiseEvents = true); + + /// + /// Deletes a UserGroup + /// + /// UserGroup to delete + void DeleteUserGroup(IUserGroup userGroup); + + #endregion } } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 8e984d1e5d..fcd8c45536 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -516,6 +516,34 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + /// + public IEnumerable GetAllInGroup(int groupId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserRepository(uow)) + { + return repository.GetAllInGroup(groupId); + } + } + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + /// + public IEnumerable GetAllNotInGroup(int groupId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserRepository(uow)) + { + return repository.GetAllNotInGroup(groupId); + } + } + #endregion #region Implementation of IUserService @@ -587,7 +615,7 @@ namespace Umbraco.Core.Services } /// - /// Gets all UserTypes or thosed specified as parameters + /// Gets all UserTypes or those specified as parameters /// /// Optional Ids of UserTypes to retrieve /// An enumerable list of @@ -687,6 +715,129 @@ namespace Umbraco.Core.Services DeletedUserType.RaiseEvent(new DeleteEventArgs(userType, false), this); } + /// + /// Gets all UserGroups or those specified as parameters + /// + /// Optional Ids of UserGroups to retrieve + /// An enumerable list of + public IEnumerable GetAllUserGroups(params int[] ids) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) + { + return repository.GetAll(ids).OrderBy(x => x.Name); + } + } + + /// + /// Gets all UserGroups for a given user + /// + /// Id of user + /// An enumerable list of + public IEnumerable GetGroupsForUser(int userId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserRepository(uow)) + { + return repository.GetGroupsForUser(userId); + } + } + + /// + /// Gets a UserGroup by its Alias + /// + /// Alias of the UserGroup to retrieve + /// + public IUserGroup GetUserGroupByAlias(string alias) + { + using (var repository = RepositoryFactory.CreateUserGroupRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Alias == alias); + var contents = repository.GetByQuery(query); + return contents.SingleOrDefault(); + } + } + + /// + /// Gets a UserGroup by its Id + /// + /// Id of the UserGroup to retrieve + /// + public IUserGroup GetUserGroupById(int id) + { + using (var repository = RepositoryFactory.CreateUserGroupRepository(UowProvider.GetUnitOfWork())) + { + return repository.Get(id); + } + } + + /// + /// Gets a UserGroup by its Name + /// + /// Name of the UserGroup to retrieve + /// + public IUserGroup GetUserGroupByName(string name) + { + using (var repository = RepositoryFactory.CreateUserGroupRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Name == name); + var contents = repository.GetByQuery(query); + return contents.SingleOrDefault(); + } + } + + /// + /// Saves a UserGroup + /// + /// UserGroup to save + /// Flag for whether to update the list of users in the group + /// List of user Ids + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events + public void SaveUserGroup(IUserGroup userGroup, bool updateUsers = false, int[] userIds = null, bool raiseEvents = true) + { + if (raiseEvents) + { + if (SavingUserGroup.IsRaisedEventCancelled(new SaveEventArgs(userGroup), this)) + return; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) + { + repository.AddOrUpdate(userGroup); + uow.Commit(); + + if (updateUsers) + { + repository.RemoveAllUsersFromGroup(userGroup.Id); + repository.AddUsersToGroup(userGroup.Id, userIds); + } + } + + if (raiseEvents) + SavedUserGroup.RaiseEvent(new SaveEventArgs(userGroup, false), this); + } + + /// + /// Deletes a UserGroup + /// + /// UserGroup to delete + public void DeleteUserGroup(IUserGroup userGroup) + { + if (DeletingUserGroup.IsRaisedEventCancelled(new DeleteEventArgs(userGroup), this)) + return; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) + { + repository.Delete(userGroup); + uow.Commit(); + } + + DeletedUserGroup.RaiseEvent(new DeleteEventArgs(userGroup, false), this); + } + /// /// Removes a specific section from all users /// @@ -768,8 +919,205 @@ namespace Umbraco.Core.Services } } + /// + /// Get permissions set for a group and optional node ids + /// + /// Group to retrieve permissions for + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + public IEnumerable GetPermissions(IUserGroup group, params int[] nodeIds) + { + throw new NotImplementedException(); + } + + /// + /// Get permissions set for a user and optional node ids + /// + /// User repository + /// User to retrieve permissions for + /// Specifiying nothing will return all user permissions for all nodes + /// An enumerable list of + private IEnumerable GetPermissions(IUserRepository repository, IUser user, params int[] nodeIds) + { + var explicitPermissions = repository.GetUserPermissionsForEntities(user.Id, nodeIds); + var result = new List(explicitPermissions); + + // Save list of nodes that user has explicit permissions for - these take priority so we don't want + // to amend with any settings from groups (we'll take the special "null" action as not being defined + // as this is set if someone sets permissions on a user and then removes them) + var explicitlyDefinedEntityIds = result + .Where(IsNotNullActionPermission) + .Select(x => x.EntityId) + .ToArray(); + + // If no permissions are assigned to a particular node then, we will fill in those permissions with the user's defaults + var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); + foreach (var id in missingIds) + { + result.Add( + new EntityPermission( + user.Id, + id, + user.DefaultPermissions.ToArray())); + } + + // Add or amend the existing permissions based on these + foreach (var group in user.Groups) + { + var groupPermissions = GetPermissions(group, nodeIds).ToList(); + foreach (var groupPermission in groupPermissions) + { + // Check if we already have this from explicitly set user permissions, if so, ignore it + if (explicitlyDefinedEntityIds.Contains(groupPermission.EntityId) == false) + { + // Add group permission, ensuring we keep a unique value for the entity Id in the list + AddOrAmendPermission(result, groupPermission); + } + } + } + + return result; + } + + private void AddOrAmendPermission(IList permissions, EntityPermission groupPermission) + { + var existingPermission = permissions + .SingleOrDefault(x => x.EntityId == groupPermission.EntityId); + if (existingPermission != null) + { + existingPermission.AddAdditionalPermissions(groupPermission.AssignedPermissions); + } + else + { + permissions.Add(groupPermission); + } + } + + /// + /// Gets the permissions for the provided user and path + /// + /// User to check permissions for + /// Path to check permissions for + /// String indicating permissions for provided user and path + public string GetPermissionsForPath(IUser user, string path) + { + var nodeId = GetNodeIdFromPath(path); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserRepository(uow)) + { + // Check for permissions directly assigned to the node for the user (if we have any, they take priority + // over anything defined on groups; we'll take the special "null" action as not being defined + // as this is set if someone sets permissions on a user and then removes them) + var permissions = GetPermissions(repository, user, nodeId) + .Where(IsNotNullActionPermission) + .ToList(); + var gotDirectlyAssignedUserPermissions = permissions.Any(); + + // If none found, and checking groups, get permissions from the groups + if (gotDirectlyAssignedUserPermissions == false) + { + foreach (var group in user.Groups) + { + var groupPermissions = GetPermissions(group, nodeId).ToList(); + permissions.AddRange(groupPermissions); + } + } + + // If none found directly on the user, get those defined on the user type + if (gotDirectlyAssignedUserPermissions == false) + { + var typePermissions = GetPermissions(repository, user, nodeId).ToList(); + permissions.AddRange(typePermissions); + } + + // Extract the net permissions from the path from the set of permissions found + string assignedPermissions; + if (TryGetAssignedPermissionsForNode(permissions, nodeId, out assignedPermissions)) + { + return assignedPermissions; + } + + // Exception to everything. If default cruds is empty and we're on root node; allow browse of root node + if (path == "-1") + { + return "F"; + } + + return string.Empty; + } + } + + /// + /// Gets the permissions for the provided group and path + /// + /// User to check permissions for + /// Path to check permissions for + /// String indicating permissions for provided user and path + public string GetPermissionsForPath(IUserGroup group, string path) + { + var nodeId = GetNodeIdFromPath(path); + + var permissions = GetPermissions(group).ToList(); + + string assignedPermissions; + TryGetAssignedPermissionsForNode(permissions, nodeId, out assignedPermissions); + return assignedPermissions; + } + + private static int GetNodeIdFromPath(string path) + { + return path.Contains(",") + ? int.Parse(path.Substring(path.LastIndexOf(",", StringComparison.Ordinal) + 1)) + : int.Parse(path); + } + + private static bool IsNotNullActionPermission(EntityPermission x) + { + const string NullActionChar = "-"; + return string.Join(string.Empty, x.AssignedPermissions) != NullActionChar; + } + + /// + /// Checks in a set of permissions associated with a user for those related to a given nodeId + /// + /// The set of permissions + /// The node Id + /// The permissions to return + /// True if permissions for the given path are found + public static bool TryGetAssignedPermissionsForNode(IList permissions, + int nodeId, + out string assignedPermissions) + { + if (permissions.Any(x => x.EntityId == nodeId)) + { + var found = permissions.First(x => x.EntityId == nodeId); + var assignedPermissionsArray = found.AssignedPermissions.ToList(); + + // Working with permissions assigned directly to a user AND to their groups, so maybe several per node + // and we need to get the most permissive set + foreach (var permission in permissions.Where(x => x.EntityId == nodeId).Skip(1)) + { + AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); + } + + assignedPermissions = string.Join("", assignedPermissionsArray); + return true; + } + + assignedPermissions = string.Empty; + return false; + } + + private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) + { + var permissionsToAdd = additionalPermissions + .Where(x => assignedPermissions.Contains(x) == false); + assignedPermissions.AddRange(permissionsToAdd); + } + #endregion - + /// /// Occurs before Save /// @@ -809,5 +1157,25 @@ namespace Umbraco.Core.Services /// Occurs after Delete /// public static event TypedEventHandler> DeletedUserType; + + /// + /// Occurs before Save + /// + public static event TypedEventHandler> SavingUserGroup; + + /// + /// Occurs after Save + /// + public static event TypedEventHandler> SavedUserGroup; + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> DeletingUserGroup; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> DeletedUserGroup; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index bf70f6afd6..555c76eb4c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -385,6 +385,9 @@ + + + @@ -412,6 +415,10 @@ + + + + @@ -421,11 +428,13 @@ + + @@ -438,6 +447,7 @@ + @@ -474,6 +484,7 @@ + @@ -489,11 +500,13 @@ + + diff --git a/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs b/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs index bed5582609..201fedfcad 100644 --- a/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs +++ b/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.Persistence private DatabaseSchemaHelper _schemaHelper; public abstract Database Database { get; } - + protected abstract ISqlSyntaxProvider SqlSyntaxProvider { get; } protected DatabaseSchemaHelper DatabaseSchemaHelper @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence public virtual void Initialize() { _logger = new Logger(new FileInfo(TestHelper.MapPathForTest("~/unit-test-log4net.config"))); - + TestHelper.InitializeContentDirectories(); string path = TestHelper.CurrentAssemblyDirectory; @@ -64,9 +64,9 @@ namespace Umbraco.Tests.Persistence new ServiceContext(repositoryFactory, new PetaPocoUnitOfWorkProvider(_logger), new FileUnitOfWorkProvider(), new PublishingStrategy(evtMsgs, _logger), cacheHelper, _logger, evtMsgs), cacheHelper, new ProfilingLogger(_logger, Mock.Of())) - { - IsReady = true - }; + { + IsReady = true + }; Resolution.Freeze(); } @@ -83,7 +83,7 @@ namespace Umbraco.Tests.Persistence //RepositoryResolver.Reset(); } - + [Test] public void Can_Create_umbracoNode_Table() @@ -135,7 +135,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoAppTree_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -147,7 +147,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsContentType2ContentType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -160,7 +160,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsContentTypeAllowedContentType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -174,7 +174,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsContentType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -187,7 +187,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsContentVersion_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -202,7 +202,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsContentXml_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -217,7 +217,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsDataType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -230,7 +230,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsDataTypePreValues_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -244,7 +244,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsDictionary_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -256,7 +256,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsLanguageText_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -270,7 +270,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsTemplate_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -283,7 +283,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsDocument_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -299,7 +299,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsDocumentType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -314,7 +314,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoDomains_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -327,7 +327,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoLanguage_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -339,7 +339,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoLog_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -351,7 +351,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsMacro_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -359,11 +359,11 @@ namespace Umbraco.Tests.Persistence //transaction.Complete(); } } - + [Test] public void Can_Create_cmsMember_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -378,7 +378,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsMember2MemberGroup_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -394,7 +394,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsMemberType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -408,7 +408,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsPreviewXml_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -424,7 +424,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsPropertyData_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -441,7 +441,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsPropertyType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -457,7 +457,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsPropertyTypeGroup_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -471,7 +471,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoRelation_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -485,7 +485,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoRelationType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -497,7 +497,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsStylesheet_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -510,7 +510,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsStylesheetProperty_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -523,7 +523,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsTags_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -535,7 +535,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsTagRelationship_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -555,7 +555,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsTask_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -571,7 +571,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_cmsTaskType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -608,7 +608,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoUser_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -621,7 +621,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoUserType_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -631,14 +631,11 @@ namespace Umbraco.Tests.Persistence } [Test] - public void Can_Create_umbracoUser2app_Table() + public void Can_Create_umbracoUserGroup_Table() { - using (Transaction transaction = Database.GetTransaction()) { - DatabaseSchemaHelper.CreateTable(); - DatabaseSchemaHelper.CreateTable(); - DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); //transaction.Complete(); } @@ -647,7 +644,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Can_Create_umbracoUser2NodeNotify_Table() { - + using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); @@ -660,15 +657,40 @@ namespace Umbraco.Tests.Persistence } [Test] - public void Can_Create_umbracoUser2NodePermission_Table() + public void Can_Create_umbracoUser2UserGroup_Table() + { + using (Transaction transaction = Database.GetTransaction()) + { + DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); + + //transaction.Complete(); + } + } + + public void Can_Create_umbracoGroupUser2app_Table() { - using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); - DatabaseSchemaHelper.CreateTable(); - DatabaseSchemaHelper.CreateTable(); - DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); + + //transaction.Complete(); + } + } + + [Test] + public void Can_Create_umbracoUserGroup2NodePermission_Table() + { + + using (Transaction transaction = Database.GetTransaction()) + { + DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); //transaction.Complete(); } diff --git a/src/Umbraco.Tests/Services/SectionServiceTests.cs b/src/Umbraco.Tests/Services/SectionServiceTests.cs new file mode 100644 index 0000000000..e574d749f4 --- /dev/null +++ b/src/Umbraco.Tests/Services/SectionServiceTests.cs @@ -0,0 +1,102 @@ +using NUnit.Framework; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using System; +using System.Linq; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Tests.Services +{ + /// + /// Tests covering the SectionService + /// + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture, RequiresSTA] + public class SectionServiceTests : BaseServiceTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + } + + public override void CreateTestData() + { + base.CreateTestData(); + + ServiceContext.SectionService.MakeNew("Content", "content", "icon-content"); + ServiceContext.SectionService.MakeNew("Media", "media", "icon-media"); + ServiceContext.SectionService.MakeNew("Settings", "settings", "icon-settings"); + ServiceContext.SectionService.MakeNew("Developer", "developer", "icon-developer"); + } + + [Test] + public void SectionService_Can_Get_Allowed_Sections_For_User() + { + // Arrange + var user = CreateUser(); + + // Act + var result = ServiceContext.SectionService.GetAllowedSections(user.Id).ToList(); + + // Assert + Assert.AreEqual(2, result.Count); + } + + [Test] + public void SectionService_Can_Get_Allowed_Sections_For_User_With_Groups() + { + // Arrange + var user = CreateUser(true); + + // Act + var result = ServiceContext.SectionService.GetAllowedSections(user.Id).ToList(); + + // Assert + Assert.AreEqual(4, result.Count); + } + + private IUser CreateUser(bool withGroups = false) + { + var userType = new UserType + { + Alias = "TypeA", + Name = "Type A", + }; + ServiceContext.UserService.SaveUserType(userType, false); + + var user = new User(userType) + { + Name = "Test user", + Username = "testUser", + Email = "testuser@test.com", + }; + user.AddAllowedSection("content"); + user.AddAllowedSection("media"); + ServiceContext.UserService.Save(user, false); + + if (withGroups) + { + var userGroupA = new UserGroup + { + Alias = "GroupA", + Name = "Group A" + }; + userGroupA.AddAllowedSection("media"); + userGroupA.AddAllowedSection("settings"); + ServiceContext.UserService.SaveUserGroup(userGroupA, true, new[] { user.Id }, false); + + var userGroupB = new UserGroup + { + Alias = "GroupB", + Name = "Group B" + }; + userGroupB.AddAllowedSection("settings"); + userGroupB.AddAllowedSection("developer"); + ServiceContext.UserService.SaveUserGroup(userGroupB, true, new[] { user.Id }, false); + } + + return ServiceContext.UserService.GetUserById(user.Id); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 8659a73cd1..89053a2d5a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -179,6 +179,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index d046ac7104..d68edb0ea4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -815,3 +815,8 @@ legend + .control-group { } } + +/* User/group selector */ +.group-selector .group-selector-list { float: left; } +.group-selector .group-selector-list div { height: 24px; } +.group-selector .group-selector-buttons { float: left; margin: 24px 16px; } diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 234b3a8f09..d679384a38 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -603,6 +603,7 @@ + diff --git a/src/Umbraco.Web.UI/config/trees.Release.config b/src/Umbraco.Web.UI/config/trees.Release.config index 9b2b8e8b6f..f53e3b7862 100644 --- a/src/Umbraco.Web.UI/config/trees.Release.config +++ b/src/Umbraco.Web.UI/config/trees.Release.config @@ -30,8 +30,9 @@ - - + + + diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 81c0c03bc8..30563647f8 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -26,7 +26,8 @@ - + + @@ -41,5 +42,5 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml index 9fca6d082a..7af0512ed7 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml @@ -1,5 +1,5 @@ - +
Template
/create/simple.ascx @@ -57,7 +57,7 @@ -
+
membergroup
/create/simple.ascx @@ -78,7 +78,7 @@ -
+
member
/create/member.ascx @@ -92,14 +92,14 @@ -
+
membergroup
/create/simple.ascx -
+
Stylesheet editor egenskab
/create/simple.ascx @@ -235,9 +235,17 @@
+ +
User Groups
+ /create/simple.ascx + + + + +
Macro
- /Create/PartialView.ascx + /Create/PartialView.ascx diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml index f6859c6423..ba3df007e7 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml @@ -221,6 +221,14 @@
+ +
User Groups
+ /create/simple.ascx + + + + +
Macro
/Create/PartialView.ascx diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index f710e54215..c4ffc9aefc 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -958,8 +958,8 @@ To manage your website, simply open the Umbraco back office and start adding con
Do not close this window during sorting]]>
- Validation - Validation errors must be fixed before the item can be saved + Validation + Validation errors must be fixed before the item can be saved Failed Insufficient user permissions, could not complete the operation Cancelled @@ -994,6 +994,7 @@ To manage your website, simply open the Umbraco back office and start adding con Error saving user (check log) User Saved User type saved + User group saved File not saved file could not be saved. Please check file permissions File saved @@ -1303,8 +1304,11 @@ To manage your website, simply open the Umbraco back office and start adding con Start Node in Content Name User permissions + User group permissions User type User types + User group + User groups Writer Translator Change diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 80c6faf07b..98075f00ac 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -993,6 +993,7 @@ To manage your website, simply open the Umbraco back office and start adding con Error saving user (check log) User Saved User type saved + User group saved File not saved file could not be saved. Please check file permissions File saved @@ -1308,8 +1309,11 @@ To manage your website, simply open the Umbraco back office and start adding con Start Node in Content Name User permissions + User group permissions User type User types + User group + User groups Writer Translator Change diff --git a/src/Umbraco.Web.UI/umbraco/users/EditUserGroup.aspx b/src/Umbraco.Web.UI/umbraco/users/EditUserGroup.aspx new file mode 100644 index 0000000000..6626f33eb1 --- /dev/null +++ b/src/Umbraco.Web.UI/umbraco/users/EditUserGroup.aspx @@ -0,0 +1,52 @@ +<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EditUserGroup.aspx.cs" MasterPageFile="../masterpages/umbracoPage.Master" + Inherits="umbraco.cms.presentation.user.EditUserGroup" %> + +<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Users not in group:
+ +
+
+ + +
+
+
Users in group:
+ +
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index f0c0861059..a6c24913ad 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -66,8 +66,8 @@ namespace Umbraco.Web.Security { get { - //only load it once per instance! - if (_currentUser == null) + //only load it once per instance! (but make sure groups are loaded) + if (_currentUser == null || _currentUser.GroupsLoaded == false) { var id = GetUserId(); if (id == -1) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 7fb022e848..aec0cdab68 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -417,6 +417,17 @@ + + + EditUserGroup.aspx + ASPXCodeBehind + + + EditUserGroup.aspx + + + ASPXCodeBehind + @@ -2029,6 +2040,7 @@ ASPXCodeBehind
+ diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs new file mode 100644 index 0000000000..98e87ce34b --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs @@ -0,0 +1,50 @@ +using System; +using umbraco.BusinessLogic; +using System.Collections.Generic; +using umbraco.businesslogic; +using Umbraco.Core; + +namespace umbraco.cms.presentation.Trees +{ + [Tree(Constants.Applications.Users, "userGroups", "User Groups", sortOrder: 2)] + public class UserGroups : BaseTree + { + + public UserGroups(string application) : base(application) { } + + public override void RenderJS(ref System.Text.StringBuilder Javascript) + { + Javascript.Append( + @" +function openUserGroups(id) { + UmbClientMgr.contentFrame('users/EditUserGroup.aspx?id=' + id); +} +"); + } + + public override void Render(ref XmlTree tree) + { + List userGroups = UserGroup.GetAllUserGroups(); + foreach (UserGroup userGroup in userGroups) + { + XmlTreeNode node = XmlTreeNode.Create(this); + node.NodeID = userGroup.Id.ToString(); + node.Action = string.Format("javascript:openUserGroups({0})", userGroup.Id.ToString()); + node.Icon = "icon-users"; + node.Text = userGroup.Name; + + OnBeforeNodeRender(ref tree, ref node, EventArgs.Empty); + if (node != null) + { + tree.Add(node); + OnAfterNodeRender(ref tree, ref node, EventArgs.Empty); + } + } + } + + protected override void CreateRootNode(ref XmlTreeNode rootNode) + { + rootNode.Text = ui.Text("user", "userGroups"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserTypes.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserTypes.cs index 8ac4771672..54dd21738b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserTypes.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserTypes.cs @@ -1,12 +1,4 @@ using System; -using System.Data; -using System.Configuration; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; using umbraco.BusinessLogic; using System.Collections.Generic; using umbraco.businesslogic; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index d9605506b4..b128903fdc 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -2,33 +2,25 @@ using System.Collections; using System.Configuration.Provider; using System.Globalization; -using System.IO; +using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; -using System.Xml; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Security; using Umbraco.Web; -using Umbraco.Web.Models; using Umbraco.Web.Security; using umbraco.BasePages; using umbraco.BusinessLogic; -using umbraco.businesslogic.Exceptions; -using umbraco.cms.businesslogic.media; using umbraco.cms.businesslogic.web; using umbraco.controls; using umbraco.presentation.channels.businesslogic; using umbraco.uicontrols; -using umbraco.providers; using umbraco.cms.presentation.Trees; using Umbraco.Core.IO; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Core.Services; using PropertyType = umbraco.cms.businesslogic.propertytype.PropertyType; namespace umbraco.cms.presentation.user @@ -36,7 +28,7 @@ namespace umbraco.cms.presentation.user /// /// Summary description for EditUser. /// - public partial class EditUser : UmbracoEnsuredPage + public partial class EditUser : EditUserGroupsBase { public EditUser() { @@ -66,6 +58,13 @@ namespace umbraco.cms.presentation.user protected ContentPicker cContentPicker = new ContentPicker(); protected CustomValidator sectionValidator = new CustomValidator(); + protected UpdatePanel pnlGroups = new UpdatePanel(); + protected PlaceHolder pnlGroupControls = new PlaceHolder(); + protected ListBox lstInGroups = new ListBox(); + protected ListBox lstNotInGroups = new ListBox(); + protected Button btnAddGroup = new Button(); + protected Button btnRemoveGroup = new Button(); + protected Pane pp = new Pane(); private User u; @@ -154,7 +153,7 @@ namespace umbraco.cms.presentation.user var passwordChanger = (passwordChanger)LoadControl(SystemDirectories.Umbraco + "/controls/passwordChanger.ascx"); passwordChanger.MembershipProviderName = UmbracoSettings.DefaultBackofficeProvider; - //Add a custom validation message for the password changer + // Add a custom validation message for the password changer var passwordValidation = new CustomValidator { ID = "PasswordChangerValidator" @@ -180,22 +179,59 @@ namespace umbraco.cms.presentation.user pp.addProperty(ui.Text("user", "usertype", UmbracoUser), userType); pp.addProperty(ui.Text("user", "language", UmbracoUser), userLanguage); - //Media / content root nodes - Pane ppNodes = new Pane(); + // Media / content root nodes + var ppNodes = new Pane(); ppNodes.addProperty(ui.Text("user", "startnode", UmbracoUser), content); ppNodes.addProperty(ui.Text("user", "mediastartnode", UmbracoUser), medias); - //Generel umrbaco access - Pane ppAccess = new Pane(); + // General umbraco access + var ppAccess = new Pane(); ppAccess.addProperty(ui.Text("user", "noConsole", UmbracoUser), NoConsole); ppAccess.addProperty(ui.Text("user", "disabled", UmbracoUser), Disabled); - //access to which modules... - Pane ppModules = new Pane(); + // Access to which modules... + var ppModules = new Pane(); ppModules.addProperty(ui.Text("user", "modules", UmbracoUser), lapps); ppModules.addProperty(" ", sectionValidator); - TabPage userInfo = UserTabs.NewTabPage(u.Name); + // Groups + var ppGroups = new Pane(); + lstNotInGroups.SelectionMode = ListSelectionMode.Multiple; + btnAddGroup.Text = "Add"; + btnAddGroup.Click += btnAddToGroup_Click; + btnRemoveGroup.Text = "Remove"; + btnRemoveGroup.Click += btnRemoveFromGroup_Click; + lstInGroups.SelectionMode = ListSelectionMode.Multiple; + pnlGroups.ContentTemplateContainer.Controls.Add(pnlGroupControls); + pnlGroups.Attributes.Add("class", "group-selector"); + + var pnl1 = new Panel(); + pnl1.CssClass = "group-selector-list"; + var pnl1Header = new Panel(); + pnl1Header.Controls.Add(new Literal { Text = "Available groups" }); + pnl1.Controls.Add(pnl1Header); + pnl1.Controls.Add(lstNotInGroups); + + var pnl2 = new Panel(); + pnl2.CssClass = "group-selector-buttons"; + pnl2.Controls.Add(btnAddGroup); + pnl2.Controls.Add(btnRemoveGroup); + + var pnl3 = new Panel(); + pnl3.CssClass = "group-selector-list"; + var pnl3Header = new Panel(); + pnl3Header.Controls.Add(new Literal { Text = "Selected groups" }); + pnl3.Controls.Add(pnl3Header); + pnl3.Controls.Add(lstInGroups); + + pnlGroups.ContentTemplateContainer.Controls.Add(pnl1); + pnlGroups.ContentTemplateContainer.Controls.Add(pnl2); + pnlGroups.ContentTemplateContainer.Controls.Add(pnl3); + + ppModules.addProperty(ui.Text("user", "userGroups", UmbracoUser), pnlGroups); + BindGroups(); + + var userInfo = UserTabs.NewTabPage(u.Name); userInfo.Controls.Add(pp); @@ -203,6 +239,7 @@ namespace umbraco.cms.presentation.user userInfo.Controls.Add(ppNodes); userInfo.Controls.Add(ppModules); + userInfo.Controls.Add(ppGroups); userInfo.HasMenu = true; @@ -227,6 +264,33 @@ namespace umbraco.cms.presentation.user .SyncTree(UID.ToString(), IsPostBack); } + private void BindGroups() + { + var userService = ApplicationContext.Current.Services.UserService; + var allGroups = userService.GetAllUserGroups(); + var groupsForUser = userService.GetGroupsForUser(u.Id); + + lstInGroups.DataSource = groupsForUser; + lstInGroups.DataValueField = "Id"; + lstInGroups.DataTextField = "Name"; + lstInGroups.DataBind(); + + lstNotInGroups.DataSource = allGroups + .Where(x => groupsForUser.Select(y => y.Id).Contains(x.Id) == false); + lstNotInGroups.DataValueField = "Id"; + lstNotInGroups.DataTextField = "Name"; + lstNotInGroups.DataBind(); + } + + protected void btnAddToGroup_Click(object sender, EventArgs e) + { + MoveItems(lstNotInGroups, lstInGroups); + } + + protected void btnRemoveFromGroup_Click(object sender, EventArgs e) + { + MoveItems(lstInGroups, lstNotInGroups); + } void sectionValidator_ServerValidate(object source, ServerValidateEventArgs args) { @@ -276,7 +340,6 @@ namespace umbraco.cms.presentation.user } // Handle content and media pickers - PlaceHolder medias = new PlaceHolder(); cMediaPicker.AppAlias = Constants.Applications.Media; cMediaPicker.TreeAlias = "media"; @@ -336,7 +399,6 @@ namespace umbraco.cms.presentation.user ///
private void SetupForm() { - if (!IsPostBack) { MembershipUser user = BackOfficeProvider.GetUser(u.LoginName, false); @@ -487,6 +549,12 @@ namespace umbraco.cms.presentation.user if (li.Selected) u.AddApplication(li.Value); } + u.ClearGroups(); + foreach (ListItem li in lstInGroups.Items) + { + u.AddGroup(int.Parse(li.Value), li.Text); + } + u.Save(); // save data diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx new file mode 100644 index 0000000000..6626f33eb1 --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx @@ -0,0 +1,52 @@ +<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EditUserGroup.aspx.cs" MasterPageFile="../masterpages/umbracoPage.Master" + Inherits="umbraco.cms.presentation.user.EditUserGroup" %> + +<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Users not in group:
+ +
+
+ + +
+
+
Users in group:
+ +
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs new file mode 100644 index 0000000000..9b929bd931 --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Web.UI.WebControls; +using umbraco.BasePages; +using umbraco.BusinessLogic; +using umbraco.cms.presentation.Trees; +using umbraco.interfaces; +using Umbraco.Core; + +namespace umbraco.cms.presentation.user +{ + public partial class EditUserGroup : EditUserGroupsBase + { + public EditUserGroup() + { + CurrentApp = BusinessLogic.DefaultApps.users.ToString(); + } + + protected void Page_Load(object sender, EventArgs e) + { + pnlUmbraco.Text = umbraco.ui.Text("usergroup", base.getUser()); + + var save = pnlUmbraco.Menu.NewButton(); + save.Click += save_Click; + save.ID = "save"; + save.ToolTip = ui.Text("save"); + save.Text = ui.Text("save"); + + pp_alias.Text = umbraco.ui.Text("alias", base.getUser()); + pp_name.Text = umbraco.ui.Text("name", base.getUser()); + + pp_rights.Text = umbraco.ui.Text("default", base.getUser()) + " " + umbraco.ui.Text("rights", base.getUser()); + pp_sections.Text = umbraco.ui.Text("sections", base.getUser()); + + pp_users.Text = umbraco.ui.Text("users", base.getUser()); + + //ensure we have a query string + if (string.IsNullOrEmpty(Request.QueryString["id"])) + return; + //ensure it is an integer + if (!int.TryParse(Request.QueryString["id"], out m_userGroupID)) + return; + + if (!IsPostBack) + { + BindDetails(); + BindActions(); + BindSections(); + BindUsers(); + ClientTools + .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) + .SyncTree(m_userGroupID.ToString(), false); + } + } + + void save_Click(object sender, EventArgs e) + { + var userGroup = CurrentUserGroup; + userGroup.Name = txtUserGroupName.Text; + string actions = ""; + + foreach (ListItem li in cbl_rights.Items) + { + if (li.Selected) + actions += li.Value; + } + + userGroup.DefaultPermissions = actions; + + var userIds = new List(); + foreach (ListItem li in lstUsersInGroup.Items) + { + userIds.Add(int.Parse(li.Value)); + } + + userGroup.ClearApplications(); + foreach (ListItem li in cbl_sections.Items) + { + if (li.Selected) userGroup.AddApplication(li.Value); + } + + userGroup.SaveWithUsers(userIds.ToArray()); + + ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "editUserGroupSaved", base.getUser()), ""); + } + + protected List CurrentUserGroupActions + { + get + { + if (m_userGroupActions == null) + m_userGroupActions = umbraco.BusinessLogic.Actions.Action.FromString(CurrentUserGroup.DefaultPermissions); + return m_userGroupActions; + } + } + + protected UserGroup CurrentUserGroup + { + get + { + if (m_userGroup == null) + m_userGroup = UserGroup.GetUserGroup(m_userGroupID); + return m_userGroup; + } + } + + private UserGroup m_userGroup; + private List m_userGroupActions; + private int m_userGroupID; + + private void BindDetails() + { + lblUserGroupAlias.Text = CurrentUserGroup.Alias; + txtUserGroupName.Text = CurrentUserGroup.Name; + hidUserGroupID.Value = CurrentUserGroup.Id.ToString(); + } + + private void BindActions() + { + foreach (IAction ai in global::umbraco.BusinessLogic.Actions.Action.GetPermissionAssignable()) + { + + ListItem li = new ListItem(umbraco.ui.Text(ai.Alias, base.getUser()), ai.Letter.ToString()); + + if (CurrentUserGroupActions.Contains(ai)) + li.Selected = true; + + cbl_rights.Items.Add(li); + } + } + + private void BindSections() + { + string currentUserApps = ";"; + foreach (Application a in CurrentUser.Applications) + currentUserApps += a.alias + ";"; + + Application[] gapps = CurrentUserGroup.Applications; + foreach (Application app in BusinessLogic.Application.getAll()) + { + if (CurrentUser.IsAdmin() || currentUserApps.Contains(";" + app.alias + ";")) + { + ListItem li = new ListItem(ui.Text("sections", app.alias), app.alias); + foreach (Application tmp in gapps) if (app.alias == tmp.alias) li.Selected = true; + cbl_sections.Items.Add(li); + } + } + } + + private void BindUsers() + { + var userService = ApplicationContext.Current.Services.UserService; + + lstUsersInGroup.DataSource = userService.GetAllInGroup(CurrentUserGroup.Id); + lstUsersInGroup.DataValueField = "Id"; + lstUsersInGroup.DataTextField = "Name"; + lstUsersInGroup.DataBind(); + + lstUsersNotInGroup.DataSource = userService.GetAllNotInGroup(CurrentUserGroup.Id); + lstUsersNotInGroup.DataValueField = "Id"; + lstUsersNotInGroup.DataTextField = "Name"; + lstUsersNotInGroup.DataBind(); + } + + protected void btnAddToGroup_Click(object sender, EventArgs e) + { + MoveItems(lstUsersNotInGroup, lstUsersInGroup); + } + + protected void btnRemoveFromGroup_Click(object sender, EventArgs e) + { + MoveItems(lstUsersInGroup, lstUsersNotInGroup); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.designer.cs new file mode 100644 index 0000000000..2de0f19ef0 --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.designer.cs @@ -0,0 +1,195 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace umbraco.cms.presentation.user { + + + public partial class EditUserGroup { + + /// + /// pnlUmbraco control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.UmbracoPanel pnlUmbraco; + + /// + /// pnl1 control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.Pane pnl1; + + /// + /// hidUserGroupID control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.HiddenField hidUserGroupID; + + /// + /// pp_name control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.PropertyPanel pp_name; + + /// + /// txtUserGroupName control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.TextBox txtUserGroupName; + + /// + /// pp_alias control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.PropertyPanel pp_alias; + + /// + /// lblUserGroupAlias control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.Label lblUserGroupAlias; + + /// + /// pnl2 control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.Pane pnl2; + + /// + /// pp_rights control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.PropertyPanel pp_rights; + + /// + /// cbl_rights control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.CheckBoxList cbl_rights; + + /// + /// pnl3 control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.Pane pnl3; + + /// + /// pp_sections control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.PropertyPanel pp_sections; + + /// + /// cbl_sections control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.CheckBoxList cbl_sections; + + /// + /// pnl4 control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.Pane pnl4; + + /// + /// pp_users control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.PropertyPanel pp_users; + + /// + /// pnlUsers control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.UpdatePanel pnlUsers; + + /// + /// lstUsersNotInGroup control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.ListBox lstUsersNotInGroup; + + /// + /// btnAddToGroup control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.Button btnAddToGroup; + + /// + /// btnRemoveFromGroup control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.Button btnRemoveFromGroup; + + /// + /// lstUsersInGroup control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.ListBox lstUsersInGroup; + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroupsBase.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroupsBase.cs new file mode 100644 index 0000000000..d5710423cf --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroupsBase.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web.UI.WebControls; +using umbraco.BasePages; + +namespace umbraco.cms.presentation.user +{ + /// + /// Base class provided common functionality to the web forms that allow for assignment of users to groups + /// + public abstract class EditUserGroupsBase : UmbracoEnsuredPage + { + protected void MoveItems(ListBox from, ListBox to) + { + for (var i = from.Items.Count - 1; i >= 0; i--) + { + if (from.Items[i].Selected) + { + to.Items.Add(from.Items[i]); + var li = from.Items[i]; + from.Items.Remove(li); + } + } + + from.SelectedIndex = -1; + to.SelectedIndex = -1; + SortItems(to); + } + + protected void SortItems(ListBox listBox) + { + // Hat-tip: http://stackoverflow.com/a/3396527 + var list = new List(listBox.Items.Cast()); + list = list.OrderBy(li => li.Text).ToList(); + listBox.Items.Clear(); + listBox.Items.AddRange(list.ToArray()); + } + } +} \ No newline at end of file diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 59b6c19a41..3a3b5f14cd 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -1,19 +1,13 @@ using System; using System.Collections; -using System.Web.Caching; using Umbraco.Core; -using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Models.Rdbms; - -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Repositories; -using umbraco.DataLayer; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; +using umbraco.DataLayer; +using Umbraco.Core.Persistence.Querying; namespace umbraco.BusinessLogic { @@ -596,32 +590,14 @@ namespace umbraco.BusinessLogic /// /// Gets the users permissions based on a nodes path /// - /// The path. + /// The path. /// - public string GetPermissions(string Path) + public string GetPermissions(string path) { if (_lazyId.HasValue) SetupUser(_lazyId.Value); - var defaultPermissions = UserType.DefaultPermissions; - - var cachedPermissions = ApplicationContext.Current.Services.UserService.GetPermissions(UserEntity) - .ToArray(); - - // NH 4.7.1 changing default permission behavior to default to User Type permissions IF no specific permissions has been - // set for the current node - var nodeId = Path.Contains(",") ? int.Parse(Path.Substring(Path.LastIndexOf(",", StringComparison.Ordinal) + 1)) : int.Parse(Path); - if (cachedPermissions.Any(x => x.EntityId == nodeId)) - { - var found = cachedPermissions.First(x => x.EntityId == nodeId); - return string.Join("", found.AssignedPermissions); - } - - // exception to everything. If default cruds is empty and we're on root node; allow browse of root node - if (string.IsNullOrEmpty(defaultPermissions) && Path == "-1") - defaultPermissions = "F"; - - // else return default user type cruds - return defaultPermissions; + var userService = ApplicationContext.Current.Services.UserService; + return userService.GetPermissionsForPath(UserEntity, path); } /// @@ -746,6 +722,32 @@ namespace umbraco.BusinessLogic ApplicationContext.Current.Services.UserService.Save(UserEntity); } + /// + /// Clears the list of groups the user is in, ensure to call Save afterwords + /// + public void ClearGroups() + { + if (_lazyId.HasValue) SetupUser(_lazyId.Value); + foreach (var group in UserEntity.Groups.ToArray()) + { + UserEntity.RemoveGroup(group); + } + } + + /// + /// Adds a group to the list groups for the user, ensure to call Save() afterwords + /// + public void AddGroup(int groupId, string groupName) + { + if (_lazyId.HasValue) SetupUser(_lazyId.Value); + UserEntity.AddGroup(new Umbraco.Core.Models.Membership.UserGroup + { + Id = groupId, + Name = groupName, + }); + } + + /// /// Gets or sets a value indicating whether the user has access to the Umbraco back end. /// diff --git a/src/umbraco.businesslogic/UserGroup.cs b/src/umbraco.businesslogic/UserGroup.cs new file mode 100644 index 0000000000..c04dad96e3 --- /dev/null +++ b/src/umbraco.businesslogic/UserGroup.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Events; + +namespace umbraco.BusinessLogic +{ + /// + /// Represents a umbraco Usergroup + /// + [Obsolete("Use the UserService instead")] + public class UserGroup + { + internal Umbraco.Core.Models.Membership.IUserGroup UserGroupItem; + + /// + /// Creates a new empty instance of a UserGroup + /// + public UserGroup() + { + UserGroupItem = new Umbraco.Core.Models.Membership.UserGroup(); + } + + internal UserGroup(Umbraco.Core.Models.Membership.IUserGroup userGroup) + { + UserGroupItem = userGroup; + } + + /// + /// Creates a new instance of a UserGroup and attempts to + /// load it's values from the database cache. + /// + /// + /// If the UserGroup is not found in the existing ID list, then this object + /// will remain an empty object + /// + /// The UserGroup id to find + public UserGroup(int id) + { + this.LoadByPrimaryKey(id); + } + + /// + /// Initializes a new instance of the class. + /// + /// The user type id. + /// The name. + public UserGroup(int id, string name) + { + UserGroupItem = new Umbraco.Core.Models.Membership.UserGroup(); + UserGroupItem.Id = id; + UserGroupItem.Name = name; + } + + /// + /// Creates a new instance of UserGroup with all parameters + /// + /// + /// + /// + /// + public UserGroup(int id, string name, string defaultPermissions, string alias) + { + UserGroupItem = new Umbraco.Core.Models.Membership.UserGroup(); + UserGroupItem.Id = id; + UserGroupItem.Name = name; + UserGroupItem.Alias = alias; + UserGroupItem.Permissions = defaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// The cache storage for all user groups + /// + private static List UserGroups + { + get + { + return ApplicationContext.Current.Services.UserService.GetAllUserGroups() + .Select(x => new UserGroup(x)) + .ToList(); + } + } + + #region Public Properties + /// + /// Gets or sets the user type alias. + /// + public string Alias + { + get { return UserGroupItem.Alias; } + set { UserGroupItem.Alias = value; } + } + + /// + /// Gets the name of the user type. + /// + public string Name + { + get { return UserGroupItem.Name; } + set { UserGroupItem.Name = value; } + } + + /// + /// Gets the id the user type + /// + public int Id + { + get { return UserGroupItem.Id; } + } + + /// + /// Gets the default permissions of the user type + /// + public string DefaultPermissions + { + get { return UserGroupItem.Permissions == null ? string.Empty : string.Join("", UserGroupItem.Permissions); } + set { UserGroupItem.Permissions = value.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); } + } + + /// + /// Gets the applications which the group has access to. + /// + /// The users applications. + public Application[] Applications + { + get + { + return GetApplications().ToArray(); + } + } + + /// + /// Get the application which the group has access to as a List + /// + /// + public List GetApplications() + { + var allApps = Application.getAll(); + var apps = new List(); + + var sections = UserGroupItem.AllowedSections; + + foreach (var s in sections) + { + var app = allApps.SingleOrDefault(x => x.alias == s); + if (app != null) + apps.Add(app); + } + + return apps; + } + + /// + /// Clears the list of applications the user has access to, ensure to call Save afterwords + /// + public void ClearApplications() + { + foreach (var s in UserGroupItem.AllowedSections.ToArray()) + { + UserGroupItem.RemoveAllowedSection(s); + } + } + + /// + /// Adds a application to the list of allowed applications, ensure to call Save() afterwords + /// + /// + public void AddApplication(string appAlias) + { + UserGroupItem.AddAllowedSection(appAlias); + } + + #endregion + + /// + /// Saves this instance + /// + public void Save() + { + PerformSave(); + } + + /// + /// Saves this instance (along with the list of users in the group if provided) + /// + public void SaveWithUsers(int[] userIds) + { + PerformSave(true, userIds); + } + + private void PerformSave(bool updateUsers = false, int[] userIds = null) + { + //ensure that this object has an ID specified (it exists in the database) + if (UserGroupItem.HasIdentity == false) + throw new Exception("The current UserGroup object does not exist in the database. New UserGroups should be created with the MakeNew method"); + + ApplicationContext.Current.Services.UserService.SaveUserGroup(UserGroupItem, updateUsers, userIds); + + //raise event + OnUpdated(this, new EventArgs()); + } + + /// + /// Deletes this instance. + /// + public void Delete() + { + //ensure that this object has an ID specified (it exists in the database) + if (UserGroupItem.HasIdentity == false) + throw new Exception("The current UserGroup object does not exist in the database. New UserGroups should be created with the MakeNew method"); + + ApplicationContext.Current.Services.UserService.DeleteUserGroup(UserGroupItem); + + //raise event + OnDeleted(this, new EventArgs()); + } + + /// + /// Load the data for the current UserGroup by it's id + /// + /// + /// Returns true if the UserGroup id was found + /// and the data was loaded, false if it wasn't + public bool LoadByPrimaryKey(int id) + { + UserGroupItem = ApplicationContext.Current.Services.UserService.GetUserGroupById(id); + return UserGroupItem != null; + } + + /// + /// Creates a new user group + /// + /// + /// + /// + public static UserGroup MakeNew(string name, string defaultPermissions, string alias) + { + //ensure that the current alias does not exist + //get the id for the new user type + var existing = UserGroups.Find(ut => (ut.Alias == alias)); + + if (existing != null) + throw new Exception("The UserGroup alias specified already exists"); + + var userType = new Umbraco.Core.Models.Membership.UserGroup + { + Alias = alias, + Name = name, + Permissions = defaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)) + }; + ApplicationContext.Current.Services.UserService.SaveUserGroup(userType); + + var legacy = new UserGroup(userType); + + //raise event + OnNew(legacy, new EventArgs()); + + return legacy; + } + + /// + /// Gets the user type with the specied ID + /// + /// The id. + /// + public static UserGroup GetUserGroup(int id) + { + return UserGroups.Find(ut => (ut.Id == id)); + } + + /// + /// Returns all user groups + /// + /// + public static List GetAllUserGroups() + { + return UserGroups; + } + + internal static event TypedEventHandler New; + private static void OnNew(UserGroup userType, EventArgs args) + { + if (New != null) + { + New(userType, args); + } + } + + internal static event TypedEventHandler Deleted; + private static void OnDeleted(UserGroup userType, EventArgs args) + { + if (Deleted != null) + { + Deleted(userType, args); + } + } + + internal static event TypedEventHandler Updated; + private static void OnUpdated(UserGroup userType, EventArgs args) + { + if (Updated != null) + { + Updated(userType, args); + } + } + } +} \ No newline at end of file diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index 1483523caf..0d6aae1b71 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -241,6 +241,7 @@ Code + Code From 2d9aeb3aa1595c715dcd5cc8ed8282259e63bda3 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 27 Oct 2016 18:11:46 +0200 Subject: [PATCH 002/714] Removed relationship between users and applications and got back to a compiling state. Set up editing screens for group/node permissions. --- src/Umbraco.Core/Cache/CacheKeys.cs | 2 + .../Models/Identity/BackOfficeIdentityUser.cs | 1 + .../Models/Identity/IdentityModelMappings.cs | 2 + .../Models/Membership/EntityPermission.cs | 6 +- .../Models/Membership/EntityPermissionSet.cs | 25 +- src/Umbraco.Core/Models/Membership/IUser.cs | 4 - src/Umbraco.Core/Models/Membership/User.cs | 92 +--- .../Membership/UserGroupEntityPermission.cs | 16 + src/Umbraco.Core/Models/Rdbms/User2AppDto.cs | 20 - .../Models/Rdbms/User2NodePermissionDto.cs | 23 - src/Umbraco.Core/Models/Rdbms/UserDto.cs | 3 - .../Persistence/Factories/UserFactory.cs | 23 - .../Factories/UserSectionFactory.cs | 35 -- .../Migrations/Initial/BaseDataCreation.cs | 16 - .../Relators/UserSectionRelator.cs | 50 --- .../Repositories/ContentRepository.cs | 8 +- .../DataTypeDefinitionRepository.cs | 2 +- .../Interfaces/IContentRepository.cs | 2 +- .../Interfaces/IUserGroupRepository.cs | 2 - .../Interfaces/IUserRepository.cs | 31 -- .../Repositories/PermissionRepository.cs | 239 +++++----- .../Repositories/UserGroupRepository.cs | 16 +- .../Repositories/UserRepository.cs | 136 +----- .../Security/BackOfficeUserStore.cs | 40 +- src/Umbraco.Core/Security/UserData.cs | 3 + src/Umbraco.Core/Services/ContentService.cs | 2 +- .../Services/ContentServiceExtensions.cs | 2 +- src/Umbraco.Core/Services/IContentService.cs | 2 +- src/Umbraco.Core/Services/ISectionService.cs | 2 +- src/Umbraco.Core/Services/IUserService.cs | 30 +- src/Umbraco.Core/Services/UserService.cs | 92 ++-- .../Services/UserServiceExtensions.cs | 13 +- src/Umbraco.Core/Umbraco.Core.csproj | 5 +- src/Umbraco.Tests/Models/UserTests.cs | 13 - .../Repositories/UserGroupRepositoryTest.cs | 425 ++++++++++++++++++ .../Repositories/UserRepositoryTest.cs | 139 +----- .../Services/SectionServiceTests.cs | 2 - .../Services/UserServiceTests.cs | 138 +++--- .../TestHelpers/Entities/MockedUser.cs | 17 +- .../TestHelpers/Entities/MockedUserGroup.cs | 17 + .../TreesAndSections/SectionTests.cs | 58 --- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../ContentControllerUnitTests.cs | 12 +- ...terAllowedOutgoingContentAttributeTests.cs | 8 +- src/Umbraco.Web.UI/config/trees.config | 2 +- .../umbraco/users/PermissionEditor.aspx | 2 +- .../Cache/CacheRefresherEventHandler.cs | 34 +- src/Umbraco.Web/Cache/DistributedCache.cs | 5 +- .../Cache/DistributedCacheExtensions.cs | 14 +- .../Cache/UserGroupCacheRefresher.cs | 66 +++ .../UserGroupPermissionsCacheRefresher.cs | 58 +++ .../Cache/UserPermissionsCacheRefresher.cs | 58 --- src/Umbraco.Web/Security/WebSecurity.cs | 1 - src/Umbraco.Web/Umbraco.Web.csproj | 16 +- ...Permissions.cs => UserGroupPermissions.cs} | 165 ++++--- .../umbraco/dialogs/cruds.aspx.cs | 72 ++- .../umbraco/dialogs/moveOrCopy.aspx.cs | 7 +- .../umbraco/users/EditUser.aspx.cs | 54 +-- .../umbraco/users/NodePermissions.ascx.cs | 103 ++--- .../umbraco/users/PermissionEditor.aspx | 2 +- .../umbraco/users/PermissionEditor.aspx.cs | 79 ++-- .../umbraco/users/PermissionsHandler.asmx.cs | 85 ++-- ...Permissions.cs => UserGroupPermissions.cs} | 292 ++++++------ src/umbraco.businesslogic/User.cs | 54 --- src/umbraco.cms/businesslogic/CMSNode.cs | 5 +- src/umbraco.cms/businesslogic/Permission.cs | 105 +++-- 66 files changed, 1430 insertions(+), 1625 deletions(-) create mode 100644 src/Umbraco.Core/Models/Membership/UserGroupEntityPermission.cs delete mode 100644 src/Umbraco.Core/Models/Rdbms/User2AppDto.cs delete mode 100644 src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs delete mode 100644 src/Umbraco.Core/Persistence/Factories/UserSectionFactory.cs delete mode 100644 src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs create mode 100644 src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs create mode 100644 src/Umbraco.Tests/TestHelpers/Entities/MockedUserGroup.cs create mode 100644 src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs create mode 100644 src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs delete mode 100644 src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs rename src/Umbraco.Web/umbraco.presentation/umbraco/Trees/{UserPermissions.cs => UserGroupPermissions.cs} (57%) rename src/Umbraco.Web/umbraco.presentation/umbraco/users/{UserPermissions.cs => UserGroupPermissions.cs} (77%) diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 0c1a202b66..596bf2455b 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -58,6 +58,8 @@ namespace Umbraco.Core.Cache public const string UserPermissionsCacheKey = "UmbracoUserPermissions"; + public const string UserGroupPermissionsCacheKey = "UmbracoUserGroupPermissions"; + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string ContentTypeCacheKey = "UmbracoContentType"; diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 50dc7d06f8..1618150851 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -34,6 +34,7 @@ namespace Umbraco.Core.Models.Identity public int StartContentId { get; set; } public int StartMediaId { get; set; } public string[] AllowedSections { get; set; } + public string[] Groups { get; set; } public string Culture { get; set; } public string UserTypeAlias { get; set; } diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 0dc95a8987..74fd602a7e 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -24,12 +24,14 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.StartContentId, expression => expression.MapFrom(user => user.StartContentId)) .ForMember(user => user.UserTypeAlias, expression => expression.MapFrom(user => user.UserType.Alias)) .ForMember(user => user.AccessFailedCount, expression => expression.MapFrom(user => user.FailedPasswordAttempts)) + .ForMember(user => user.Groups, expression => expression.MapFrom(user => user.Groups.Select(x => x.Name).ToArray())) .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); config.CreateMap() .ConstructUsing((BackOfficeIdentityUser user) => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id' .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) + .ForMember(detail => detail.Groups, opt => opt.MapFrom(user => user.Groups)) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => new[] { user.UserTypeAlias })) .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs index 520a9b8d37..90430e26a8 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs @@ -3,18 +3,16 @@ namespace Umbraco.Core.Models.Membership { /// - /// Represents a user -> entity permission + /// Represents an entity permission (defined on the user group and derived to retrieve permissions for a given user) /// public class EntityPermission { - public EntityPermission(int userId, int entityId, string[] assignedPermissions) + public EntityPermission(int entityId, string[] assignedPermissions) { - UserId = userId; EntityId = entityId; AssignedPermissions = assignedPermissions; } - public int UserId { get; private set; } public int EntityId { get; private set; } /// diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs index c4669caf59..ea1e927a59 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs @@ -13,30 +13,31 @@ namespace Umbraco.Core.Models.Membership public int EntityId { get; private set; } /// - /// The key/value pairs of user id & single permission + /// The key/value pairs of user group id & single permission /// - public IEnumerable UserPermissionsSet { get; private set; } + public IEnumerable PermissionsSet { get; private set; } - public EntityPermissionSet(int entityId, IEnumerable userPermissionsSet) + public EntityPermissionSet(int entityId, IEnumerable permissionsSet) { EntityId = entityId; - UserPermissionsSet = userPermissionsSet; + PermissionsSet = permissionsSet; } - public class UserPermission + public class UserGroupPermission { - public UserPermission(int userId, string permission) + public UserGroupPermission(int groupId, string permission) { - UserId = userId; + UserGroupId = groupId; Permission = permission; } - public int UserId { get; private set; } + public int UserGroupId { get; private set; } + public string Permission { get; private set; } - protected bool Equals(UserPermission other) + protected bool Equals(UserGroupPermission other) { - return UserId == other.UserId && string.Equals(Permission, other.Permission); + return UserGroupId == other.UserGroupId && string.Equals(Permission, other.Permission); } public override bool Equals(object obj) @@ -44,14 +45,14 @@ namespace Umbraco.Core.Models.Membership if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((UserPermission) obj); + return Equals((UserGroupPermission) obj); } public override int GetHashCode() { unchecked { - return (UserId*397) ^ Permission.GetHashCode(); + return (UserGroupId * 397) ^ Permission.GetHashCode(); } } } diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 095044c47e..a3cea0b232 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -47,10 +47,6 @@ namespace Umbraco.Core.Models.Membership IEnumerable AllowedSections { get; } - void RemoveAllowedSection(string sectionAlias); - - void AddAllowedSection(string sectionAlias); - /// /// Exposes the basic profile data /// diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 6fad1a6f78..41bcb5ee80 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -25,12 +25,8 @@ namespace Umbraco.Core.Models.Membership _defaultPermissions = _userType.Permissions == null ? Enumerable.Empty() : new List(_userType.Permissions); //Groups = new List { userType }; SessionTimeout = 60; - _sectionCollection = new ObservableCollection(); - _addedSections = new List(); - _removedSections = new List(); _groupCollection = new List(); _language = GlobalSettings.DefaultUILanguage; - _sectionCollection.CollectionChanged += SectionCollectionChanged; _isApproved = true; _isLockedOut = false; _startContentId = -1; @@ -55,9 +51,6 @@ namespace Umbraco.Core.Models.Membership private IUserType _userType; private string _name; private string _securityStamp; - private List _addedSections; - private List _removedSections; - private ObservableCollection _sectionCollection; private List _groupCollection; private bool _groupsLoaded; private int _sessionTimeout; @@ -92,7 +85,6 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentId); public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); - public readonly PropertyInfo AllowedSectionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedSections); public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo UsernameSelector = ExpressionHelper.GetPropertyInfo(x => x.Username); @@ -200,22 +192,16 @@ namespace Umbraco.Core.Models.Membership public IEnumerable AllowedSections { - get { return _sectionCollection; } - } - - public void RemoveAllowedSection(string sectionAlias) - { - if (_sectionCollection.Contains(sectionAlias)) + get { - _sectionCollection.Remove(sectionAlias); - } - } + if (GroupsLoaded == false) + { + return Enumerable.Empty(); + } - public void AddAllowedSection(string sectionAlias) - { - if (_sectionCollection.Contains(sectionAlias) == false) - { - _sectionCollection.Add(sectionAlias); + return Groups + .SelectMany(x => x.AllowedSections) + .Distinct(); } } @@ -234,22 +220,6 @@ namespace Umbraco.Core.Models.Membership set { SetPropertyValueAndDetectChanges(value, ref _securityStamp, Ps.Value.SecurityStampSelector); } } - /// - /// Used internally to check if we need to add a section in the repository to the db - /// - internal IEnumerable AddedSections - { - get { return _addedSections; } - } - - /// - /// Used internally to check if we need to remove a section in the repository to the db - /// - internal IEnumerable RemovedSections - { - get { return _removedSections; } - } - /// /// Gets or sets the session timeout. /// @@ -363,61 +333,15 @@ namespace Umbraco.Core.Models.Membership #endregion - /// - /// Whenever resetting occurs, clear the remembered add/removed collections, even if - /// rememberPreviouslyChangedProperties is true, the AllowedSections property will still - /// be flagged as dirty. - /// - /// - public override void ResetDirtyProperties(bool rememberPreviouslyChangedProperties) - { - _addedSections.Clear(); - _removedSections.Clear(); - base.ResetDirtyProperties(rememberPreviouslyChangedProperties); - } - - /// - /// Handles the collection changed event in order for us to flag the AllowedSections property as changed - /// - /// - /// - void SectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(Ps.Value.AllowedSectionsSelector); - - if (e.Action == NotifyCollectionChangedAction.Add) - { - var item = e.NewItems.Cast().First(); - - if (_addedSections.Contains(item) == false) - { - _addedSections.Add(item); - } - } - else if (e.Action == NotifyCollectionChangedAction.Remove) - { - var item = e.OldItems.Cast().First(); - - if (_removedSections.Contains(item) == false) - { - _removedSections.Add(item); - } - } - } - public override object DeepClone() { var clone = (User)base.DeepClone(); //turn off change tracking clone.DisableChangeTracking(); //need to create new collections otherwise they'll get copied by ref - clone._addedSections = new List(); - clone._removedSections = new List(); - clone._sectionCollection = new ObservableCollection(_sectionCollection.ToList()); clone._groupCollection = new List(_groupCollection.ToList()); clone._defaultPermissions = new List(_defaultPermissions.ToList()); //re-create the event handler - clone._sectionCollection.CollectionChanged += clone.SectionCollectionChanged; //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); //re-enable tracking diff --git a/src/Umbraco.Core/Models/Membership/UserGroupEntityPermission.cs b/src/Umbraco.Core/Models/Membership/UserGroupEntityPermission.cs new file mode 100644 index 0000000000..619223f01a --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserGroupEntityPermission.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Models.Membership +{ + /// + /// Represents a user group -> entity permission + /// + public class UserGroupEntityPermission : EntityPermission + { + public UserGroupEntityPermission(int groupId, int entityId, string[] assignedPermissions) + : base(entityId, assignedPermissions) + { + UserGroupId = groupId; + } + + public int UserGroupId { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/User2AppDto.cs b/src/Umbraco.Core/Models/Rdbms/User2AppDto.cs deleted file mode 100644 index eafc6be230..0000000000 --- a/src/Umbraco.Core/Models/Rdbms/User2AppDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseAnnotations; - -namespace Umbraco.Core.Models.Rdbms -{ - [TableName("umbracoUser2app")] - [PrimaryKey("user", autoIncrement = false)] - [ExplicitColumns] - internal class User2AppDto - { - [Column("user")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2app", OnColumns = "user, app")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } - - [Column("app")] - [Length(50)] - public string AppAlias { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs b/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs deleted file mode 100644 index 1e6662735f..0000000000 --- a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseAnnotations; - -namespace Umbraco.Core.Models.Rdbms -{ - [TableName("umbracoUser2NodePermission")] - [PrimaryKey("userId", autoIncrement = false)] - [ExplicitColumns] - internal class User2NodePermissionDto - { - [Column("userId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUser2NodePermission", OnColumns = "userId, nodeId, permission")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } - - [Column("permission")] - public string Permission { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 05a93c86bf..d05cf1086b 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -73,8 +73,5 @@ namespace Umbraco.Core.Models.Rdbms [Column("lastLoginDate")] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastLoginDate { get; set; } - - [ResultColumn] - public List User2AppDtos { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 322e8b5b65..a404c43450 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -43,14 +43,6 @@ namespace Umbraco.Core.Persistence.Factories user.LastLoginDate = dto.LastLoginDate ?? DateTime.MinValue; user.LastPasswordChangeDate = dto.LastPasswordChangeDate ?? DateTime.MinValue; - if (dto.User2AppDtos != null) - { - foreach (var app in dto.User2AppDtos) - { - user.AddAllowedSection(app.AppAlias); - } - } - //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 user.ResetDirtyProperties(false); @@ -77,7 +69,6 @@ namespace Umbraco.Core.Persistence.Factories UserLanguage = entity.Language, UserName = entity.Name, Type = short.Parse(entity.UserType.Id.ToString(CultureInfo.InvariantCulture)), - User2AppDtos = new List(), SecurityStampToken = entity.SecurityStamp, FailedLoginAttempts = entity.FailedPasswordAttempts, LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? (DateTime?)null : entity.LastLockoutDate, @@ -85,20 +76,6 @@ namespace Umbraco.Core.Persistence.Factories LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, }; - foreach (var app in entity.AllowedSections) - { - var appDto = new User2AppDto - { - AppAlias = app - }; - if (entity.HasIdentity) - { - appDto.UserId = entity.Id; - } - - dto.User2AppDtos.Add(appDto); - } - if (entity.HasIdentity) { dto.Id = entity.Id.SafeCast(); diff --git a/src/Umbraco.Core/Persistence/Factories/UserSectionFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserSectionFactory.cs deleted file mode 100644 index 0e6a17594f..0000000000 --- a/src/Umbraco.Core/Persistence/Factories/UserSectionFactory.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using AutoMapper; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Models.Rdbms; - -namespace Umbraco.Core.Persistence.Factories -{ - internal class UserSectionFactory - { - private readonly IUser _user; - - public UserSectionFactory(IUser user) - { - _user = user; - } - - public IEnumerable BuildEntity(IEnumerable dto) - { - return dto.Select(x => x.AppAlias); - } - - public IEnumerable BuildDto(IEnumerable entity) - { - return entity.Select(x => new User2AppDto - { - //NOTE: We're force casting to int here! this might not work in the future - UserId = (int)_user.Id, - AppAlias = x - }); - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 9570024b09..68d43a1acd 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -48,11 +48,6 @@ namespace Umbraco.Core.Persistence.Migrations.Initial CreateUmbracoUserTypeData(); } - if (tableName.Equals("umbracoUser2app")) - { - CreateUmbracoUser2AppData(); - } - if (tableName.Equals("cmsPropertyTypeGroup")) { CreateCmsPropertyTypeGroupData(); @@ -168,17 +163,6 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 4, Alias = "translator", Name = "Translator", DefaultPermissions = "AF" }); } - private void CreateUmbracoUser2AppData() - { - _database.Insert("umbracoUser2app", "user", false, new User2AppDto { UserId = 0, AppAlias = Constants.Applications.Content }); - _database.Insert("umbracoUser2app", "user", false, new User2AppDto { UserId = 0, AppAlias = Constants.Applications.Developer }); - _database.Insert("umbracoUser2app", "user", false, new User2AppDto { UserId = 0, AppAlias = Constants.Applications.Media }); - _database.Insert("umbracoUser2app", "user", false, new User2AppDto { UserId = 0, AppAlias = Constants.Applications.Members }); - _database.Insert("umbracoUser2app", "user", false, new User2AppDto { UserId = 0, AppAlias = Constants.Applications.Settings }); - _database.Insert("umbracoUser2app", "user", false, new User2AppDto { UserId = 0, AppAlias = Constants.Applications.Users }); - _database.Insert("umbracoUser2app", "user", false, new User2AppDto { UserId = 0, AppAlias = Constants.Applications.Forms }); - } - private void CreateCmsPropertyTypeGroupData() { _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 3, ContentTypeNodeId = 1032, Text = "Image", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Image) }); diff --git a/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs b/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs deleted file mode 100644 index 923348e729..0000000000 --- a/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core.Models.Rdbms; - -namespace Umbraco.Core.Persistence.Relators -{ - internal class UserSectionRelator - { - internal UserDto Current; - - internal UserDto Map(UserDto a, User2AppDto p) - { - // Terminating call. Since we can return null from this function - // we need to be ready for PetaPoco to callback later with null - // parameters - if (a == null) - return Current; - - // Is this the same DictionaryItem as the current one we're processing - if (Current != null && Current.Id == a.Id) - { - if (p.AppAlias.IsNullOrWhiteSpace() == false) - { - // Yes, just add this User2AppDto to the current item's collection - Current.User2AppDtos.Add(p); - } - - // Return null to indicate we're not done with this User yet - return null; - } - - // This is a different User to the current one, or this is the - // first time through and we don't have one yet - - // Save the current User - var prev = Current; - - // Setup the new current User - Current = a; - Current.User2AppDtos = new List(); - //this can be null since we are doing a left join - if (p.AppAlias.IsNullOrWhiteSpace() == false) - { - Current.User2AppDtos.Add(p); - } - - // Return the now populated previous User (or null if first time through) - return prev; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index cafbabd286..03b3b9e6e9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -394,12 +394,12 @@ namespace Umbraco.Core.Persistence.Repositories // user's default permissions. if (parentPermissions.Any()) { - var userPermissions = ( + var userGroupPermissions = ( from perm in parentPermissions from p in perm.AssignedPermissions - select new EntityPermissionSet.UserPermission(perm.UserId, p)).ToList(); + select new EntityPermissionSet.UserGroupPermission(perm.UserGroupId, p)).ToList(); - permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userPermissions)); + permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userGroupPermissions)); //flag the entity's permissions changed flag so we can track those changes. //Currently only used for the cache refreshers to detect if we should refresh all user permissions cache. ((Content)entity).PermissionsChanged = true; @@ -791,7 +791,7 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; repo.AssignEntityPermission(entity, permission, userIds); } - public IEnumerable GetPermissionsForEntity(int entityId) + public IEnumerable GetPermissionsForEntity(int entityId) { var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); return repo.GetPermissionsForEntity(entityId); diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index a4d71885f8..b95d3f0d62 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -248,7 +248,7 @@ AND umbracoNode.id <> @id", Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); //Remove Permissions - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); //Remove associated tags Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index e2555995d2..5f74a21d7a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -60,7 +60,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - IEnumerable GetPermissionsForEntity(int entityId); + IEnumerable GetPermissionsForEntity(int entityId); /// /// Used to add/update published xml for the content item diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs index e50d265228..45abdc70b4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs @@ -24,7 +24,6 @@ namespace Umbraco.Core.Persistence.Repositories /// Ids of users void AddUsersToGroup(int groupId, int[] userIds); - /* /// /// Gets the group permissions for the specified entities /// @@ -47,6 +46,5 @@ namespace Umbraco.Core.Persistence.Repositories /// Permissions as enumerable list of /// Specify the nodes to replace permissions for void AssignGroupPermission(int groupId, char permission, params int[] entityIds); - */ } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs index 5645cafa2a..620350f563 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs @@ -22,12 +22,6 @@ namespace Umbraco.Core.Persistence.Repositories /// bool Exists(string username); - /// - /// This is useful when an entire section is removed from config - /// - /// - IEnumerable GetUsersAssignedToSection(string sectionAlias); - /// /// Gets all groups for a given user /// @@ -57,30 +51,5 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, Expression> orderBy); - - - /// - /// Gets the user permissions for the specified entities - /// - /// - /// - /// - IEnumerable GetUserPermissionsForEntities(int userId, params int[] entityIds); - - /// - /// Replaces the same permission set for a single user to any number of entities - /// - /// - /// - /// - void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds); - - /// - /// Assigns the same permission set for a single user to any number of entities - /// - /// - /// - /// - void AssignUserPermission(int userId, char permission, params int[] entityIds); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index aa671dccec..83c31f6651 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -15,6 +15,15 @@ using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Services; using CacheKeys = Umbraco.Core.Cache.CacheKeys; using Umbraco.Core.Cache; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { @@ -38,196 +47,227 @@ namespace Umbraco.Core.Persistence.Repositories } /// - /// Returns permissions for a given user for any number of nodes + /// Returns permissions for a given group for any number of nodes /// - /// + /// /// /// - public IEnumerable GetUserPermissionsForEntities(int userId, params int[] entityIds) + public IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds) { - var entityIdKey = string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); + var entityIdKey = GetEntityIdKey(entityIds); return _runtimeCache.GetCacheItem>( - string.Format("{0}{1}{2}", CacheKeys.UserPermissionsCacheKey, userId, entityIdKey), + string.Format("{0}{1}{2}", + CacheKeys.UserGroupPermissionsCacheKey, + groupId, + entityIdKey), () => - { - - var whereBuilder = new StringBuilder(); - - //where userId = @userId AND - whereBuilder.Append(_sqlSyntax.GetQuotedColumnName("userId")); - whereBuilder.Append("="); - whereBuilder.Append(userId); - - if (entityIds.Any()) - { - whereBuilder.Append(" AND "); - - //where nodeId = @nodeId1 OR nodeId = @nodeId2, etc... - whereBuilder.Append("("); - for (var index = 0; index < entityIds.Length; index++) - { - var entityId = entityIds[index]; - whereBuilder.Append(_sqlSyntax.GetQuotedColumnName("nodeId")); - whereBuilder.Append("="); - whereBuilder.Append(entityId); - if (index < entityIds.Length - 1) - { - whereBuilder.Append(" OR "); - } - } - whereBuilder.Append(")"); - } - + { + var whereCriteria = GetPermissionsForEntitiesCriteria(groupId, entityIds); var sql = new Sql(); sql.Select("*") - .From() - .Where(whereBuilder.ToString()); - - //ToArray() to ensure it's all fetched from the db once. - var result = _unitOfWork.Database.Fetch(sql).ToArray(); + .From() + .Where(whereCriteria); + var result = _unitOfWork.Database.Fetch(sql).ToArray(); + // ToArray() to ensure it's all fetched from the db once return ConvertToPermissionList(result); - }, - //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will only have this exist in cache for 20 minutes, - // then it will refresh from the database. - new TimeSpan(0, 20, 0), - //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will make this priority below average - priority: CacheItemPriority.BelowNormal); + GetCacheTimeout(), + priority: GetCachePriority()); + } - } + private static string GetEntityIdKey(int[] entityIds) + { + return string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); + } + + private string GetPermissionsForEntitiesCriteria(int groupId, params int[] entityIds) + { + var whereBuilder = new StringBuilder(); + whereBuilder.Append(_sqlSyntax.GetQuotedColumnName("userGroupId")); + whereBuilder.Append("="); + whereBuilder.Append(groupId); + + if (entityIds.Any()) + { + whereBuilder.Append(" AND "); + + //where nodeId = @nodeId1 OR nodeId = @nodeId2, etc... + whereBuilder.Append("("); + for (var index = 0; index < entityIds.Length; index++) + { + var entityId = entityIds[index]; + whereBuilder.Append(_sqlSyntax.GetQuotedColumnName("nodeId")); + whereBuilder.Append("="); + whereBuilder.Append(entityId); + if (index < entityIds.Length - 1) + { + whereBuilder.Append(" OR "); + } + } + whereBuilder.Append(")"); + } + + return whereBuilder.ToString(); + } + + private static TimeSpan GetCacheTimeout() + { + //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will only have this exist in cache for 20 minutes, + // then it will refresh from the database. + return new TimeSpan(0, 20, 0); + } + + private static CacheItemPriority GetCachePriority() + { + //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will make this priority below average + return CacheItemPriority.BelowNormal; + } + + private static IEnumerable ConvertToPermissionList(IEnumerable result) + { + var permissions = new List(); + var nodePermissions = result.GroupBy(x => x.NodeId); + foreach (var np in nodePermissions) + { + var userGroupPermissions = np.GroupBy(x => x.UserGroupId); + foreach (var permission in userGroupPermissions) + { + var perms = permission.Select(x => x.Permission).ToArray(); + permissions.Add(new UserGroupEntityPermission(permission.Key, permission.First().NodeId, perms)); + } + } + + return permissions; + } /// - /// Returns permissions for all users for a given entity + /// Returns permissions for all groups for a given entity /// /// /// - public IEnumerable GetPermissionsForEntity(int entityId) + public IEnumerable GetPermissionsForEntity(int entityId) { var sql = new Sql(); sql.Select("*") - .From() - .Where(dto => dto.NodeId == entityId) - .OrderBy(dto => dto.NodeId); + .From() + .Where(dto => dto.NodeId == entityId) + .OrderBy(dto => dto.NodeId); - //ToArray() to ensure it's all fetched from the db once. - var result = _unitOfWork.Database.Fetch(sql).ToArray(); + var result = _unitOfWork.Database.Fetch(sql).ToArray(); + // ToArray() to ensure it's all fetched from the db once return ConvertToPermissionList(result); } /// - /// Assigns the same permission set for a single user to any number of entities + /// Assigns the same permission set for a single group to any number of entities /// - /// + /// /// /// /// /// This will first clear the permissions for this user and entities and recreate them /// - public void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds) + public void ReplacePermissions(int groupId, IEnumerable permissions, params int[] entityIds) { var db = _unitOfWork.Database; using (var trans = db.GetTransaction()) { //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)"; foreach (var idGroup in entityIds.InGroupsOf(2000)) { - db.Execute("DELETE FROM umbracoUser2NodePermission WHERE userId=@userId AND nodeId in (@nodeIds)", - new { userId = userId, nodeIds = idGroup }); + db.Execute(sql, new {groupId = groupId, nodeIds = idGroup}); } - var toInsert = new List(); + var toInsert = new List(); foreach (var p in permissions) { foreach (var e in entityIds) { - toInsert.Add(new User2NodePermissionDto + toInsert.Add(new UserGroup2NodePermissionDto { NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), - UserId = userId + UserGroupId = groupId }); } } _unitOfWork.Database.BulkInsertRecords(toInsert, trans); - trans.Complete(); - //Raise the event AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(toInsert), false), this); + new SaveEventArgs(ConvertToPermissionList(toInsert), false), this); } } /// /// Assigns one permission for a user to many entities /// - /// + /// /// /// - public void AssignUserPermission(int userId, char permission, params int[] entityIds) + public void AssignPermission(int groupId, char permission, params int[] entityIds) { var db = _unitOfWork.Database; using (var trans = db.GetTransaction()) { - db.Execute("DELETE FROM umbracoUser2NodePermission WHERE userId=@userId AND permission=@permission AND nodeId in (@entityIds)", + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)"; + db.Execute(sql, new { - userId = userId, + groupId = groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds = entityIds }); - var actions = entityIds.Select(id => new User2NodePermissionDto + var actions = entityIds.Select(id => new UserGroup2NodePermissionDto { NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), - UserId = userId + UserGroupId = groupId }).ToArray(); _unitOfWork.Database.BulkInsertRecords(actions, trans); - trans.Complete(); - //Raise the event AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); + new SaveEventArgs(ConvertToPermissionList(actions), false), this); } - } + } /// - /// Assigns one permission to an entity for multiple users + /// Assigns one permission to an entity for multiple groups /// /// /// - /// - public void AssignEntityPermission(TEntity entity, char permission, IEnumerable userIds) + /// + public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) { var db = _unitOfWork.Database; using (var trans = db.GetTransaction()) { - db.Execute("DELETE FROM umbracoUser2NodePermission WHERE nodeId=@nodeId AND permission=@permission AND userId in (@userIds)", + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)"; + db.Execute(sql, new { - nodeId = entity.Id, + nodeId = entity.Id, permission = permission.ToString(CultureInfo.InvariantCulture), - userIds = userIds + groupIds = groupIds }); - var actions = userIds.Select(id => new User2NodePermissionDto + var actions = groupIds.Select(id => new UserGroup2NodePermissionDto { NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), - UserId = id + UserGroupId = id }).ToArray(); _unitOfWork.Database.BulkInsertRecords(actions, trans); - trans.Complete(); - //Raise the event AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); + new SaveEventArgs(ConvertToPermissionList(actions), false), this); } } @@ -244,41 +284,24 @@ namespace Umbraco.Core.Persistence.Repositories var db = _unitOfWork.Database; using (var trans = db.GetTransaction()) { - db.Execute("DELETE FROM umbracoUser2NodePermission WHERE nodeId=@nodeId", new { nodeId = permissionSet.EntityId }); + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId"; + db.Execute(sql, new {nodeId = permissionSet.EntityId}); - var actions = permissionSet.UserPermissionsSet.Select(p => new User2NodePermissionDto + var actions = permissionSet.PermissionsSet.Select(p => new UserGroup2NodePermissionDto { NodeId = permissionSet.EntityId, Permission = p.Permission, - UserId = p.UserId + UserGroupId = p.UserGroupId }).ToArray(); _unitOfWork.Database.BulkInsertRecords(actions, trans); - trans.Complete(); - //Raise the event AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); - } - } - - private static IEnumerable ConvertToPermissionList(IEnumerable result) - { - var permissions = new List(); - var nodePermissions = result.GroupBy(x => x.NodeId); - foreach (var np in nodePermissions) - { - var userPermissions = np.GroupBy(x => x.UserId); - foreach (var up in userPermissions) - { - var perms = up.Select(x => x.Permission).ToArray(); - permissions.Add(new EntityPermission(up.Key, up.First().NodeId, perms)); - } + new SaveEventArgs(ConvertToPermissionList(actions), false), this); } - return permissions; } - public static event TypedEventHandler, SaveEventArgs> AssignedPermissions; + public static event TypedEventHandler, SaveEventArgs> AssignedPermissions; } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index ba47270379..b13033c7b3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -76,16 +76,15 @@ namespace Umbraco.Core.Persistence.Repositories } } - /* /// /// Gets the group permissions for the specified entities /// /// Id of group /// Array of entity Ids - public IEnumerable GetGroupPermissionsForEntities(int groupId, params int[] entityIds) + public IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds) { - var repo = new GroupPermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - return repo.GetGroupPermissionsForEntities(groupId, entityIds); + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + return repo.GetPermissionsForEntities(groupId, entityIds); } /// @@ -96,8 +95,8 @@ namespace Umbraco.Core.Persistence.Repositories /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. public void ReplaceGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds) { - var repo = new GroupPermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.ReplaceGroupPermissions(groupId, permissions, entityIds); + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + repo.ReplacePermissions(groupId, permissions, entityIds); } /// @@ -108,10 +107,9 @@ namespace Umbraco.Core.Persistence.Repositories /// Specify the nodes to replace permissions for public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) { - var repo = new GroupPermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.AssignGroupPermission(groupId, permission, entityIds); + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + repo.AssignPermission(groupId, permission, entityIds); } - */ #region Overrides of RepositoryBase diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 72d20ff660..f5e3fda7c5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -40,7 +40,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var dto = Database.Fetch(new UserSectionRelator().Map, sql).FirstOrDefault(); + var dto = Database.Fetch(sql).FirstOrDefault(); if (dto == null) return null; @@ -74,7 +74,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoUser.id in (@ids)", new {ids = ids}); } - return ConvertFromDtos(Database.Fetch(new UserSectionRelator().Map, sql)) + return ConvertFromDtos(Database.Fetch(sql)) .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. } @@ -84,7 +84,7 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - var dtos = Database.Fetch(new UserSectionRelator().Map, sql) + var dtos = Database.Fetch(sql) .DistinctBy(x => x.Id); var users = ConvertFromDtos(dtos) @@ -121,9 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = new Sql(); sql.Select(columns) - .From() - .LeftJoin() - .On(left => left.Id, right => right.UserId); + .From(); return sql; } @@ -139,9 +137,8 @@ namespace Umbraco.Core.Persistence.Repositories { "DELETE FROM cmsTask WHERE userId = @Id", "DELETE FROM cmsTask WHERE parentUserId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE userId = @Id", + "DELETE FROM umbracoUser2UserGroup WHERE userId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE userId = @Id", - "DELETE FROM umbracoUser2app WHERE " + SqlSyntax.GetQuotedColumnName("user") + "=@Id", "DELETE FROM umbracoUser WHERE id = @Id", "DELETE FROM umbracoExternalLogin WHERE id = @Id" }; @@ -168,13 +165,6 @@ namespace Umbraco.Core.Persistence.Repositories var id = Convert.ToInt32(Database.Insert(userDto)); entity.Id = id; - foreach (var sectionDto in userDto.User2AppDtos) - { - //need to set the id explicitly here - sectionDto.UserId = id; - Database.Insert(sectionDto); - } - entity.ResetDirtyProperties(); } @@ -238,52 +228,23 @@ namespace Umbraco.Core.Persistence.Repositories { Database.Update(userDto, changedCols); } - - //update the sections if they've changed + + //update the groups var user = (User)entity; - if (user.IsPropertyDirty("AllowedSections")) + + //first delete all + Database.Delete("WHERE UserId = @UserId", + new { UserId = user.Id }); + + //then re-add any associated with the user + foreach (var group in user.Groups) { - //now we need to delete any applications that have been removed - foreach (var section in user.RemovedSections) + var dto = new User2UserGroupDto { - //we need to manually delete this record because it has a composite key - Database.Delete("WHERE app = @Section AND " + SqlSyntax.GetQuotedColumnName("user") + "= @UserId", - new { Section = section, UserId = user.Id }); - } - - //for any that exist on the object, we need to determine if we need to update or insert - //NOTE: the User2AppDtos collection wil always be equal to the User.AllowedSections - foreach (var sectionDto in userDto.User2AppDtos) - { - //if something has been added then insert it - if (user.AddedSections.Contains(sectionDto.AppAlias)) - { - //we need to insert since this was added - Database.Insert(sectionDto); - } - else - { - //we need to manually update this record because it has a composite key - Database.Update("SET app=@Section WHERE app=@Section AND " + SqlSyntax.GetQuotedColumnName("user") + "=@UserId", - new { Section = sectionDto.AppAlias, UserId = sectionDto.UserId }); - } - } - - //update the groups - //first delete all - Database.Delete("WHERE UserId = @UserId", - new { UserId = user.Id }); - - //then re-add any associated with the user - foreach (var group in user.Groups) - { - var dto = new User2UserGroupDto - { - UserGroupId = group.Id, - UserId = user.Id - }; - Database.Insert(dto); - } + UserGroupId = group.Id, + UserId = user.Id + }; + Database.Insert(dto); } entity.ResetDirtyProperties(); @@ -316,29 +277,6 @@ namespace Umbraco.Core.Persistence.Repositories return Database.ExecuteScalar(sql) > 0; } - public IEnumerable GetUsersAssignedToSection(string sectionAlias) - { - //Here we're building up a query that looks like this, a sub query is required because the resulting structure - // needs to still contain all of the section rows per user. - - //SELECT * - //FROM [umbracoUser] - //LEFT JOIN [umbracoUser2app] - //ON [umbracoUser].[id] = [umbracoUser2app].[user] - //WHERE umbracoUser.id IN (SELECT umbracoUser.id - // FROM [umbracoUser] - // LEFT JOIN [umbracoUser2app] - // ON [umbracoUser].[id] = [umbracoUser2app].[user] - // WHERE umbracoUser2app.app = 'content') - - var sql = GetBaseQuery(false); - var innerSql = GetBaseQuery("umbracoUser.id"); - innerSql.Where("umbracoUser2app.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); - sql.Where(string.Format("umbracoUser.id IN ({0})", innerSql.SQL)); - - return ConvertFromDtos(Database.Fetch(new UserSectionRelator().Map, sql)); - } - /// /// Gets all groups for a given user /// @@ -462,42 +400,6 @@ namespace Umbraco.Core.Persistence.Repositories return ids.Length == 0 ? Enumerable.Empty() : GetAll(ids).OrderBy(x => x.Id); } - /// - /// Returns permissions for a given user for any number of nodes - /// - /// - /// - /// - public IEnumerable GetUserPermissionsForEntities(int userId, params int[] entityIds) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - return repo.GetUserPermissionsForEntities(userId, entityIds); - } - - /// - /// Replaces the same permission set for a single user to any number of entities - /// - /// - /// - /// - public void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.ReplaceUserPermissions(userId, permissions, entityIds); - } - - /// - /// Assigns the same permission set for a single user to any number of entities - /// - /// - /// - /// - public void AssignUserPermission(int userId, char permission, params int[] entityIds) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.AssignUserPermission(userId, permission, entityIds); - } - #endregion private IEnumerable ConvertFromDtos(IEnumerable dtos) diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index c6714e256a..fcdf623dba 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -389,7 +389,7 @@ namespace Umbraco.Core.Security /// - /// Adds a user to a role (section) + /// Adds a user to a role (user group) /// /// /// @@ -406,18 +406,19 @@ namespace Umbraco.Core.Security throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } - var found = _userService.GetUserById(asInt.Result); + var foundUser = _userService.GetUserById(asInt.Result); + var foundGroup = _userService.GetUserGroupByName(roleName); - if (found != null) + if (foundUser != null && foundGroup != null) { - found.AddAllowedSection(roleName); + foundUser.AddGroup(foundGroup); } return Task.FromResult(0); } /// - /// Removes the role (allowed section) for the user + /// Removes the role (user group) for the user /// /// /// @@ -434,18 +435,19 @@ namespace Umbraco.Core.Security throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } - var found = _userService.GetUserById(asInt.Result); + var foundUser = _userService.GetUserById(asInt.Result); + var foundGroup = _userService.GetUserGroupByName(roleName); - if (found != null) + if (foundUser != null && foundGroup != null) { - found.RemoveAllowedSection(roleName); + foundUser.RemoveGroup(foundGroup); } return Task.FromResult(0); } /// - /// Returns the roles for this user + /// Returns the roles (user groups) for this user /// /// /// @@ -453,7 +455,7 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); - return Task.FromResult((IList)user.AllowedSections.ToList()); + return Task.FromResult((IList)user.Groups.ToList()); } /// @@ -465,7 +467,7 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); - return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); + return Task.FromResult(user.Groups.InvariantContains(roleName)); } /// @@ -677,17 +679,21 @@ namespace Umbraco.Core.Security user.SecurityStamp = identityUser.SecurityStamp; } - if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false - || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) + if (user.Groups.Select(x => x.Name).ContainsAll(identityUser.Groups) == false + || identityUser.Groups.ContainsAll(user.Groups.Select(x => x.Name)) == false) { anythingChanged = true; - foreach (var allowedSection in user.AllowedSections) + foreach (var group in user.Groups) { - user.RemoveAllowedSection(allowedSection); + user.RemoveGroup(group); } - foreach (var allowedApplication in identityUser.AllowedSections) + foreach (var group in identityUser.Groups) { - user.AddAllowedSection(allowedApplication); + var foundGroup = _userService.GetUserGroupByName(group); + if (foundGroup != null) + { + user.AddGroup(foundGroup); + } } } diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs index 407d2782dd..b1a8b08507 100644 --- a/src/Umbraco.Core/Security/UserData.cs +++ b/src/Umbraco.Core/Security/UserData.cs @@ -56,6 +56,9 @@ namespace Umbraco.Core.Security [DataMember(Name = "allowedApps")] public string[] AllowedApplications { get; set; } + [DataMember(Name = "groups")] + public string[] Groups { get; set; } + [DataMember(Name = "culture")] public string Culture { get; set; } } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 38b7aec9d7..8aa7030a8f 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -126,7 +126,7 @@ namespace Umbraco.Core.Services /// /// /// - public IEnumerable GetPermissionsForEntity(IContent content) + public IEnumerable GetPermissionsForEntity(IContent content) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index 4919a81217..9c56cf28eb 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Services /// public static void RemoveContentPermissions(this IContentService contentService, int contentId) { - contentService.ReplaceContentPermissions(new EntityPermissionSet(contentId, Enumerable.Empty())); + contentService.ReplaceContentPermissions(new EntityPermissionSet(contentId, Enumerable.Empty())); } /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index ec5db8c4fb..6ee384c413 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -135,7 +135,7 @@ namespace Umbraco.Core.Services /// /// /// - IEnumerable GetPermissionsForEntity(IContent content); + IEnumerable GetPermissionsForEntity(IContent content); bool SendToPublication(IContent content, int userId = 0); diff --git a/src/Umbraco.Core/Services/ISectionService.cs b/src/Umbraco.Core/Services/ISectionService.cs index df5c89d4bf..d726179224 100644 --- a/src/Umbraco.Core/Services/ISectionService.cs +++ b/src/Umbraco.Core/Services/ISectionService.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core.Services IEnumerable
GetSections(); /// - /// Get the user's allowed sections + /// Get the user group's allowed sections /// /// /// diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 2a3ebf9828..fa69cb30a3 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -48,19 +48,19 @@ namespace Umbraco.Core.Services IUser GetUserById(int id); /// - /// Removes a specific section from all users + /// Removes a specific section from all user groups /// /// This is useful when an entire section is removed from config /// Alias of the section to remove - void DeleteSectionFromAllUsers(string sectionAlias); - + void DeleteSectionFromAllUserGroups(string sectionAlias); + /// - /// Add a specific section to all users or those specified as parameters + /// Add a specific section to all user groups or those specified as parameters /// - /// This is useful when a new section is created to allow specific users accessing it + /// This is useful when a new section is created to allow specific user groups to access it /// Alias of the section to add - /// Specifiying nothing will add the section to all user - void AddSectionToAllUsers(string sectionAlias, params int[] userIds); + /// Specifiying nothing will add the section to all user + void AddSectionToAllUserGroups(string sectionAlias, params int[] groupIds); /// /// Get permissions set for a user and optional node ids @@ -96,24 +96,24 @@ namespace Umbraco.Core.Services string GetPermissionsForPath(IUserGroup group, string path); /// - /// Replaces the same permission set for a single user to any number of entities + /// Replaces the same permission set for a single group to any number of entities /// - /// Id of the user + /// Id of the group /// /// Permissions as enumerable list of , - /// if no permissions are specified then all permissions for this node are removed for this user + /// if no permissions are specified then all permissions for this node are removed for this group /// /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. - /// If no 'entityIds' are specified all permissions will be removed for the specified user. - void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds); + /// If no 'entityIds' are specified all permissions will be removed for the specified group. + void ReplaceUserGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds); /// - /// Assigns the same permission set for a single user to any number of entities + /// Assigns the same permission set for a single user group to any number of entities /// - /// Id of the user + /// Id of the group /// /// Specify the nodes to replace permissions for - void AssignUserPermission(int userId, char permission, params int[] entityIds); + void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds); /// /// Gets a list of objects associated with a given group diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index fcd8c45536..1a5abb8ee7 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -147,9 +147,6 @@ namespace Umbraco.Core.Services IsLockedOut = false, IsApproved = true }; - //adding default sections content and media - user.AddAllowedSection("content"); - user.AddAllowedSection("media"); if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(user), this)) return user; @@ -584,33 +581,33 @@ namespace Umbraco.Core.Services } /// - /// Replaces the same permission set for a single user to any number of entities + /// Replaces the same permission set for a single group to any number of entities /// - /// If no 'entityIds' are specified all permissions will be removed for the specified user. - /// Id of the user + /// If no 'entityIds' are specified all permissions will be removed for the specified group. + /// Id of the group /// Permissions as enumerable list of /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. - public void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds) + public void ReplaceUserGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds) { var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateUserRepository(uow)) + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) { - repository.ReplaceUserPermissions(userId, permissions, entityIds); + repository.ReplaceGroupPermissions(groupId, permissions, entityIds); } } /// - /// Assigns the same permission set for a single user to any number of entities + /// Assigns the same permission set for a single user group to any number of entities /// - /// Id of the user + /// Id of the user group /// /// Specify the nodes to replace permissions for - public void AssignUserPermission(int userId, char permission, params int[] entityIds) + public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) { var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateUserRepository(uow)) + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) { - repository.AssignUserPermission(userId, permission, entityIds); + repository.AssignGroupPermission(groupId, permission, entityIds); } } @@ -843,48 +840,50 @@ namespace Umbraco.Core.Services /// /// This is useful when an entire section is removed from config /// Alias of the section to remove - public void DeleteSectionFromAllUsers(string sectionAlias) + public void DeleteSectionFromAllUserGroups(string sectionAlias) { var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateUserRepository(uow)) + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) { - var assignedUsers = repository.GetUsersAssignedToSection(sectionAlias); - foreach (var user in assignedUsers) + var assignedGroups = repository.GetGroupsAssignedToSection(sectionAlias); + foreach (var group in assignedGroups) { //now remove the section for each user and commit - user.RemoveAllowedSection(sectionAlias); - repository.AddOrUpdate(user); + group.RemoveAllowedSection(sectionAlias); + repository.AddOrUpdate(group); } + uow.Commit(); } } - + /// - /// Add a specific section to all users or those specified as parameters + /// Add a specific section to all user groups or those specified as parameters /// - /// This is useful when a new section is created to allow specific users accessing it + /// This is useful when a new section is created to allow specific user groups to access it /// Alias of the section to add - /// Specifiying nothing will add the section to all user - public void AddSectionToAllUsers(string sectionAlias, params int[] userIds) + /// Specifiying nothing will add the section to all user + public void AddSectionToAllUserGroups(string sectionAlias, params int[] groupIds) { var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateUserRepository(uow)) + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) { - IEnumerable users; - if (userIds.Any()) + IEnumerable groups; + if (groupIds.Any()) { - users = repository.GetAll(userIds); + groups = repository.GetAll(groupIds); } else { - users = repository.GetAll(); + groups = repository.GetAll(); } - foreach (var user in users.Where(u => !u.AllowedSections.InvariantContains(sectionAlias))) + foreach (var group in groups.Where(g => g.AllowedSections.InvariantContains(sectionAlias) == false)) { - //now add the section for each user and commit - user.AddAllowedSection(sectionAlias); - repository.AddOrUpdate(user); + //now add the section for each group and commit + group.AddAllowedSection(sectionAlias); + repository.AddOrUpdate(group); } + uow.Commit(); } } @@ -901,16 +900,15 @@ namespace Umbraco.Core.Services var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { - var explicitPermissions = repository.GetUserPermissionsForEntities(user.Id, nodeIds); + // TODO: rework (to use groups - currently defaulting to user type) //if no permissions are assigned to a particular node then we will fill in those permissions with the user's defaults - var result = new List(explicitPermissions); + var result = new List(); var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); foreach (var id in missingIds) { result.Add( new EntityPermission( - user.Id, id, user.DefaultPermissions.ToArray())); } @@ -939,16 +937,9 @@ namespace Umbraco.Core.Services /// An enumerable list of private IEnumerable GetPermissions(IUserRepository repository, IUser user, params int[] nodeIds) { - var explicitPermissions = repository.GetUserPermissionsForEntities(user.Id, nodeIds); - var result = new List(explicitPermissions); + var result = new List(); - // Save list of nodes that user has explicit permissions for - these take priority so we don't want - // to amend with any settings from groups (we'll take the special "null" action as not being defined - // as this is set if someone sets permissions on a user and then removes them) - var explicitlyDefinedEntityIds = result - .Where(IsNotNullActionPermission) - .Select(x => x.EntityId) - .ToArray(); + // TODO: rework to groups first, then user type // If no permissions are assigned to a particular node then, we will fill in those permissions with the user's defaults var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); @@ -956,7 +947,6 @@ namespace Umbraco.Core.Services { result.Add( new EntityPermission( - user.Id, id, user.DefaultPermissions.ToArray())); } @@ -967,12 +957,8 @@ namespace Umbraco.Core.Services var groupPermissions = GetPermissions(group, nodeIds).ToList(); foreach (var groupPermission in groupPermissions) { - // Check if we already have this from explicitly set user permissions, if so, ignore it - if (explicitlyDefinedEntityIds.Contains(groupPermission.EntityId) == false) - { - // Add group permission, ensuring we keep a unique value for the entity Id in the list - AddOrAmendPermission(result, groupPermission); - } + // Add group permission, ensuring we keep a unique value for the entity Id in the list + AddOrAmendPermission(result, groupPermission); } } diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index afb679afa1..a1f480a1f5 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -10,21 +10,21 @@ namespace Umbraco.Core.Services /// Remove all permissions for this user for all nodes specified /// /// - /// + /// /// - public static void RemoveUserPermissions(this IUserService userService, int userId, params int[] entityIds) + public static void RemoveUserGroupPermissions(this IUserService userService, int groupId, params int[] entityIds) { - userService.ReplaceUserPermissions(userId, new char[] {}, entityIds); + userService.ReplaceUserGroupPermissions(groupId, new char[] {}, entityIds); } /// /// Remove all permissions for this user for all nodes /// /// - /// - public static void RemoveUserPermissions(this IUserService userService, int userId) + /// + public static void RemoveUserGroupPermissions(this IUserService userService, int groupId) { - userService.ReplaceUserPermissions(userId, new char[] { }); + userService.ReplaceUserGroupPermissions(groupId, new char[] { }); } /// @@ -62,7 +62,6 @@ namespace Umbraco.Core.Services member.ProviderUserKey == null ? member.UserName : member.ProviderUserKey.ToString(), Guid.NewGuid().ToString("N"), //pass cannot be empty writer); - user.AddAllowedSection(Constants.Applications.Content); userService.Save(user); return user; } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 555c76eb4c..07256fda05 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -385,6 +385,7 @@ + @@ -787,7 +788,6 @@ - @@ -1034,7 +1034,6 @@ - @@ -1205,9 +1204,7 @@ - - diff --git a/src/Umbraco.Tests/Models/UserTests.cs b/src/Umbraco.Tests/Models/UserTests.cs index 189a4a17a8..9f974ee9fa 100644 --- a/src/Umbraco.Tests/Models/UserTests.cs +++ b/src/Umbraco.Tests/Models/UserTests.cs @@ -41,8 +41,6 @@ namespace Umbraco.Tests.Models Username = "username" }; - item.AddAllowedSection("test"); - var clone = (User)item.DeepClone(); Assert.AreNotSame(clone, item); @@ -61,17 +59,6 @@ namespace Umbraco.Tests.Models { Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(item, null)); } - - //ensure internal collections are differet - Assert.AreNotSame(item.AddedSections, clone.AddedSections); - Assert.AreNotSame(item.RemovedSections, clone.RemovedSections); - - //ensure event handlers are still wired on clone - clone.AddAllowedSection("blah"); - Assert.AreEqual(1, clone.AddedSections.Count()); - clone.RemoveAllowedSection("blah"); - Assert.AreEqual(1, clone.RemovedSections.Count()); - } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs new file mode 100644 index 0000000000..a29aeb8c29 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs @@ -0,0 +1,425 @@ +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence; + +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture] + public class UserGroupRepositoryTest : BaseDatabaseFactoryTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + private UserGroupRepository CreateRepository(IDatabaseUnitOfWork unitOfWork) + { + return new UserGroupRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); + } + + [Test] + public void Can_Perform_Add_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + + var userType = MockedUserGroup.CreateUserGroup(); + + // Act + repository.AddOrUpdate(userType); + unitOfWork.Commit(); + + // Assert + Assert.That(userType.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + + var userType1 = MockedUserGroup.CreateUserGroup("1"); + var userType2 = MockedUserGroup.CreateUserGroup("2"); + + // Act + repository.AddOrUpdate(userType1); + unitOfWork.Commit(); + repository.AddOrUpdate(userType2); + unitOfWork.Commit(); + + // Assert + Assert.That(userType1.HasIdentity, Is.True); + Assert.That(userType2.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Verify_Fresh_Entity_Is_Not_Dirty() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var userType = MockedUserGroup.CreateUserGroup(); + repository.AddOrUpdate(userType); + unitOfWork.Commit(); + + // Act + var resolved = repository.Get(userType.Id); + bool dirty = ((UserGroup) resolved).IsDirty(); + + // Assert + Assert.That(dirty, Is.False); + } + } + + [Test] + public void Can_Perform_Update_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var userType = MockedUserGroup.CreateUserGroup(); + repository.AddOrUpdate(userType); + unitOfWork.Commit(); + + // Act + var resolved = repository.Get(userType.Id); + resolved.Name = "New Name"; + resolved.Permissions = new[]{"Z", "Y", "X"}; + repository.AddOrUpdate(resolved); + unitOfWork.Commit(); + var updatedItem = repository.Get(userType.Id); + + // Assert + Assert.That(updatedItem.Id, Is.EqualTo(resolved.Id)); + Assert.That(updatedItem.Name, Is.EqualTo(resolved.Name)); + Assert.That(updatedItem.Permissions, Is.EqualTo(resolved.Permissions)); + } + } + + [Test] + public void Can_Perform_Delete_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + + var userType = MockedUserGroup.CreateUserGroup(); + + // Act + repository.AddOrUpdate(userType); + unitOfWork.Commit(); + var id = userType.Id; + + using (var repository2 = new UserGroupRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax)) + { + repository2.Delete(userType); + unitOfWork.Commit(); + + var resolved = repository2.Get(id); + + // Assert + Assert.That(resolved, Is.Null); + } + + } + } + + [Test] + public void Can_Perform_Get_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var userType = MockedUserGroup.CreateUserGroup(); + repository.AddOrUpdate(userType); + unitOfWork.Commit(); + + // Act + var resolved = repository.Get(userType.Id); + + // Assert + Assert.That(resolved.Id, Is.EqualTo(userType.Id)); + //Assert.That(resolved.CreateDate, Is.GreaterThan(DateTime.MinValue)); + //Assert.That(resolved.UpdateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(resolved.Name, Is.EqualTo(userType.Name)); + Assert.That(resolved.Alias, Is.EqualTo(userType.Alias)); + Assert.That(resolved.Permissions, Is.EqualTo(userType.Permissions)); + } + } + + [Test] + public void Can_Perform_GetByQuery_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + CreateAndCommitMultipleUserGroups(repository, unitOfWork); + + // Act + var query = Query.Builder.Where(x => x.Alias == "testUserGroup1"); + var result = repository.GetByQuery(query); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(1)); + } + } + + [Test] + public void Can_Perform_GetAll_By_Param_Ids_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var userTypes = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + + // Act + var result = repository.GetAll(userTypes[0].Id, userTypes[1].Id); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Any(), Is.True); + Assert.That(result.Count(), Is.EqualTo(2)); + } + } + + [Test] + public void Can_Perform_GetAll_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + CreateAndCommitMultipleUserGroups(repository, unitOfWork); + + // Act + var result = repository.GetAll(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Any(), Is.True); + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(3)); + } + } + + [Test] + public void Can_Perform_Exists_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var userTypes = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + + // Act + var exists = repository.Exists(userTypes[0].Id); + + // Assert + Assert.That(exists, Is.True); + } + } + + [Test] + public void Can_Perform_Count_On_UserGroupRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var userTypes = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + + // Act + var query = Query.Builder.Where(x => x.Alias == "testUserGroup1" || x.Alias == "testUserGroup2"); + var result = repository.Count(query); + + // Assert + Assert.That(result, Is.GreaterThanOrEqualTo(2)); + } + } + + [Test] + public void Can_Remove_Section_For_Group() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var groups = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + + // Act + + //add and remove a few times, this tests the internal collection + groups[0].RemoveAllowedSection("content"); + groups[0].RemoveAllowedSection("content"); + groups[0].AddAllowedSection("content"); + groups[0].RemoveAllowedSection("content"); + + groups[1].RemoveAllowedSection("media"); + groups[1].RemoveAllowedSection("media"); + + repository.AddOrUpdate(groups[0]); + repository.AddOrUpdate(groups[1]); + unitOfWork.Commit(); + + // Assert + var result = repository.GetAll((int)groups[0].Id, (int)groups[1].Id).ToArray(); + Assert.AreEqual(1, result[0].AllowedSections.Count()); + Assert.AreEqual("media", result[0].AllowedSections.First()); + Assert.AreEqual(1, result[1].AllowedSections.Count()); + Assert.AreEqual("content", result[1].AllowedSections.First()); + } + } + + [Test] + public void Can_Add_Section_ForGroup() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var groups = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + + // Act + + //add and remove a few times, this tests the internal collection + groups[0].AddAllowedSection("settings"); + groups[0].AddAllowedSection("settings"); + groups[0].RemoveAllowedSection("settings"); + groups[0].AddAllowedSection("settings"); + + groups[1].AddAllowedSection("developer"); + + //add the same even though it's already there + groups[2].AddAllowedSection("content"); + + repository.AddOrUpdate(groups[0]); + repository.AddOrUpdate(groups[1]); + unitOfWork.Commit(); + + // Assert + var result = repository.GetAll((int)groups[0].Id, (int)groups[1].Id, (int)groups[2].Id).ToArray(); + Assert.AreEqual(3, result[0].AllowedSections.Count()); + Assert.IsTrue(result[0].AllowedSections.Contains("content")); + Assert.IsTrue(result[0].AllowedSections.Contains("media")); + Assert.IsTrue(result[0].AllowedSections.Contains("settings")); + Assert.AreEqual(3, result[1].AllowedSections.Count()); + Assert.IsTrue(result[1].AllowedSections.Contains("content")); + Assert.IsTrue(result[1].AllowedSections.Contains("media")); + Assert.IsTrue(result[1].AllowedSections.Contains("developer")); + Assert.AreEqual(2, result[2].AllowedSections.Count()); + Assert.IsTrue(result[1].AllowedSections.Contains("content")); + Assert.IsTrue(result[1].AllowedSections.Contains("media")); + } + } + + [Test] + public void Can_Update_Section_For_Group() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var groups = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + + // Act + + groups[0].RemoveAllowedSection("content"); + groups[0].AddAllowedSection("settings"); + + repository.AddOrUpdate(groups[0]); + unitOfWork.Commit(); + + // Assert + var result = repository.Get((int)groups[0].Id); + Assert.AreEqual(2, result.AllowedSections.Count()); + Assert.IsTrue(result.AllowedSections.Contains("settings")); + Assert.IsTrue(result.AllowedSections.Contains("media")); + } + } + + + [Test] + public void Get_Groups_Assigned_To_Section() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var user1 = MockedUserGroup.CreateUserGroup("1", new[] { "media" }); + var user2 = MockedUserGroup.CreateUserGroup("2", new[] { "settings" }); + var user3 = MockedUserGroup.CreateUserGroup("3", new[] { "settings" }); + repository.AddOrUpdate(user1); + repository.AddOrUpdate(user2); + repository.AddOrUpdate(user3); + unitOfWork.Commit(); + + // Act + + var groups = repository.GetGroupsAssignedToSection("test"); + + // Assert + Assert.AreEqual(2, groups.Count()); + var names = groups.Select(x => x.Name).ToArray(); + Assert.IsTrue(names.Contains("TestGroup1")); + Assert.IsTrue(names.Contains("TestGroup3")); + } + } + + private IUserGroup[] CreateAndCommitMultipleUserGroups(IUserGroupRepository repository, IUnitOfWork unitOfWork) + { + var userGroup1 = MockedUserGroup.CreateUserGroup("1"); + var userGroup2 = MockedUserGroup.CreateUserGroup("2"); + var userGroup3 = MockedUserGroup.CreateUserGroup("3"); + repository.AddOrUpdate(userGroup1); + repository.AddOrUpdate(userGroup2); + repository.AddOrUpdate(userGroup3); + unitOfWork.Commit(); + return new IUserGroup[] { userGroup1, userGroup2, userGroup3 }; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 1d26e59113..2a92cb39c8 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -135,7 +135,6 @@ namespace Umbraco.Tests.Persistence.Repositories resolved.StartMediaId = 11; resolved.Email = "new@new.com"; resolved.Username = "newName"; - resolved.RemoveAllowedSection("content"); repository.AddOrUpdate(resolved); unitOfWork.Commit(); @@ -341,142 +340,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - [Test] - public void Can_Remove_Section_For_User() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - UserTypeRepository userTypeRepository; - using (var repository = CreateRepository(unitOfWork, out userTypeRepository)) - { - var users = CreateAndCommitMultipleUsers(repository, unitOfWork); - - // Act - - //add and remove a few times, this tests the internal collection - users[0].RemoveAllowedSection("content"); - users[0].RemoveAllowedSection("content"); - users[0].AddAllowedSection("content"); - users[0].RemoveAllowedSection("content"); - - users[1].RemoveAllowedSection("media"); - users[1].RemoveAllowedSection("media"); - - repository.AddOrUpdate(users[0]); - repository.AddOrUpdate(users[1]); - unitOfWork.Commit(); - - // Assert - var result = repository.GetAll((int) users[0].Id, (int) users[1].Id).ToArray(); - Assert.AreEqual(1, result[0].AllowedSections.Count()); - Assert.AreEqual("media", result[0].AllowedSections.First()); - Assert.AreEqual(1, result[1].AllowedSections.Count()); - Assert.AreEqual("content", result[1].AllowedSections.First()); - } - } - - [Test] - public void Can_Add_Section_For_User() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - UserTypeRepository userTypeRepository; - using (var repository = CreateRepository(unitOfWork, out userTypeRepository)) - { - var users = CreateAndCommitMultipleUsers(repository, unitOfWork); - - // Act - - //add and remove a few times, this tests the internal collection - users[0].AddAllowedSection("settings"); - users[0].AddAllowedSection("settings"); - users[0].RemoveAllowedSection("settings"); - users[0].AddAllowedSection("settings"); - - users[1].AddAllowedSection("developer"); - - //add the same even though it's already there - users[2].AddAllowedSection("content"); - - repository.AddOrUpdate(users[0]); - repository.AddOrUpdate(users[1]); - unitOfWork.Commit(); - - // Assert - var result = repository.GetAll((int) users[0].Id, (int) users[1].Id, (int) users[2].Id).ToArray(); - Assert.AreEqual(3, result[0].AllowedSections.Count()); - Assert.IsTrue(result[0].AllowedSections.Contains("content")); - Assert.IsTrue(result[0].AllowedSections.Contains("media")); - Assert.IsTrue(result[0].AllowedSections.Contains("settings")); - Assert.AreEqual(3, result[1].AllowedSections.Count()); - Assert.IsTrue(result[1].AllowedSections.Contains("content")); - Assert.IsTrue(result[1].AllowedSections.Contains("media")); - Assert.IsTrue(result[1].AllowedSections.Contains("developer")); - Assert.AreEqual(2, result[2].AllowedSections.Count()); - Assert.IsTrue(result[1].AllowedSections.Contains("content")); - Assert.IsTrue(result[1].AllowedSections.Contains("media")); - } - } - - [Test] - public void Can_Update_Section_For_User() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - UserTypeRepository userTypeRepository; - using (var repository = CreateRepository(unitOfWork, out userTypeRepository)) - { - var users = CreateAndCommitMultipleUsers(repository, unitOfWork); - - // Act - - users[0].RemoveAllowedSection("content"); - users[0].AddAllowedSection("settings"); - - repository.AddOrUpdate(users[0]); - unitOfWork.Commit(); - - // Assert - var result = repository.Get((int) users[0].Id); - Assert.AreEqual(2, result.AllowedSections.Count()); - Assert.IsTrue(result.AllowedSections.Contains("settings")); - Assert.IsTrue(result.AllowedSections.Contains("media")); - } - } - - - [Test] - public void Get_Users_Assigned_To_Section() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - UserTypeRepository userTypeRepository; - using (var repository = CreateRepository(unitOfWork, out userTypeRepository)) - { - var user1 = MockedUser.CreateUser(CreateAndCommitUserType(), "1", "test", "media"); - var user2 = MockedUser.CreateUser(CreateAndCommitUserType(), "2", "media", "settings"); - var user3 = MockedUser.CreateUser(CreateAndCommitUserType(), "3", "test", "settings"); - repository.AddOrUpdate(user1); - repository.AddOrUpdate(user2); - repository.AddOrUpdate(user3); - unitOfWork.Commit(); - - // Act - - var users = repository.GetUsersAssignedToSection("test"); - - // Assert - Assert.AreEqual(2, users.Count()); - var names = users.Select(x => x.Username).ToArray(); - Assert.IsTrue(names.Contains("TestUser1")); - Assert.IsTrue(names.Contains("TestUser3")); - } - } - [Test] public void Default_User_Permissions_Based_On_User_Type() { @@ -488,7 +351,7 @@ namespace Umbraco.Tests.Persistence.Repositories { // Act - var user1 = MockedUser.CreateUser(CreateAndCommitUserType(), "1", "test", "media"); + var user1 = MockedUser.CreateUser(CreateAndCommitUserType(), "1"); repository.AddOrUpdate(user1); unitOfWork.Commit(); diff --git a/src/Umbraco.Tests/Services/SectionServiceTests.cs b/src/Umbraco.Tests/Services/SectionServiceTests.cs index e574d749f4..3bfba08e76 100644 --- a/src/Umbraco.Tests/Services/SectionServiceTests.cs +++ b/src/Umbraco.Tests/Services/SectionServiceTests.cs @@ -71,8 +71,6 @@ namespace Umbraco.Tests.Services Username = "testUser", Email = "testuser@test.com", }; - user.AddAllowedSection("content"); - user.AddAllowedSection("media"); ServiceContext.UserService.Save(user, false); if (withGroups) diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index c7002ce79d..35013ca69b 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -185,7 +185,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10); + var users = MockedUser.CreateMulipleUsers(userType, 10); ServiceContext.UserService.Save(users); //don't find this var customUser = MockedUser.CreateUser(userType); @@ -203,7 +203,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10); + var users = MockedUser.CreateMulipleUsers(userType, 10); ServiceContext.UserService.Save(users); //include this var customUser = MockedUser.CreateUser(userType); @@ -221,7 +221,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10); + var users = MockedUser.CreateMulipleUsers(userType, 10); ServiceContext.UserService.Save(users); //include this var customUser = MockedUser.CreateUser(userType); @@ -239,7 +239,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10); + var users = MockedUser.CreateMulipleUsers(userType, 10); ServiceContext.UserService.Save(users); //include this var customUser = MockedUser.CreateUser(userType); @@ -257,7 +257,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10); + var users = MockedUser.CreateMulipleUsers(userType, 10); ServiceContext.UserService.Save(users); int totalRecs; @@ -275,7 +275,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10); + var users = MockedUser.CreateMulipleUsers(userType, 10); ServiceContext.UserService.Save(users); var customUser = MockedUser.CreateUser(userType); ServiceContext.UserService.Save(customUser); @@ -292,7 +292,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10, (i, member) => member.LastLoginDate = DateTime.Now.AddMinutes(i * -2)); + var users = MockedUser.CreateMulipleUsers(userType, 10, (i, member) => member.LastLoginDate = DateTime.Now.AddMinutes(i * -2)); ServiceContext.UserService.Save(users); var customUser = MockedUser.CreateUser(userType); @@ -304,7 +304,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10, (i, member) => member.IsLockedOut = i % 2 == 0); + var users = MockedUser.CreateMulipleUsers(userType, 10, (i, member) => member.IsLockedOut = i % 2 == 0); ServiceContext.UserService.Save(users); var customUser = MockedUser.CreateUser(userType); @@ -321,7 +321,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var users = MockedUser.CreateUser(userType, 10, (i, member) => member.IsApproved = i % 2 == 0); + var users = MockedUser.CreateMulipleUsers(userType, 10, (i, member) => member.IsApproved = i % 2 == 0); ServiceContext.UserService.Save(users); var customUser = MockedUser.CreateUser(userType); @@ -392,102 +392,124 @@ namespace Umbraco.Tests.Services } [Test] - public void Can_Add_And_Remove_Sections_From_User() + public void Can_Add_And_Remove_Sections_From_UserGroup() { - var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); + var userGroup = new UserGroup + { + Id = 1, + Alias = "Group1", + }; + userGroup.AddAllowedSection("content"); + userGroup.AddAllowedSection("mediat"); + ServiceContext.UserService.SaveUserGroup(userGroup); - var user1 = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", userType); + var result1 = ServiceContext.UserService.GetUserGroupById(userGroup.Id); - var result1 = ServiceContext.UserService.GetUserById((int)user1.Id); - //expect 2 sections by default Assert.AreEqual(2, result1.AllowedSections.Count()); //adds some allowed sections - user1.AddAllowedSection("test1"); - user1.AddAllowedSection("test2"); - user1.AddAllowedSection("test3"); - user1.AddAllowedSection("test4"); - ServiceContext.UserService.Save(user1); + userGroup.AddAllowedSection("test1"); + userGroup.AddAllowedSection("test2"); + userGroup.AddAllowedSection("test3"); + userGroup.AddAllowedSection("test4"); + ServiceContext.UserService.SaveUserGroup(userGroup); + + result1 = ServiceContext.UserService.GetUserGroupById(userGroup.Id); - result1 = ServiceContext.UserService.GetUserById((int)user1.Id); - //expect 6 sections including the two default sections Assert.AreEqual(6, result1.AllowedSections.Count()); //simulate clearing the sections - foreach (var s in user1.AllowedSections) + foreach (var s in userGroup.AllowedSections) { result1.RemoveAllowedSection(s); } + //now just re-add a couple result1.AddAllowedSection("test3"); result1.AddAllowedSection("test4"); - ServiceContext.UserService.Save(result1); + ServiceContext.UserService.SaveUserGroup(result1); //assert //re-get - result1 = ServiceContext.UserService.GetUserById((int)user1.Id); + result1 = ServiceContext.UserService.GetUserGroupById(userGroup.Id); Assert.AreEqual(2, result1.AllowedSections.Count()); } [Test] - public void Can_Remove_Section_From_All_Assigned_Users() - { - var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); + public void Can_Remove_Section_From_All_Assigned_UserGroups() + { + var userGroup1 = new UserGroup + { + Id = 1, + Alias = "Group1", + }; + var userGroup2 = new UserGroup + { + Id = 2, + Alias = "Group2", + }; + ServiceContext.UserService.SaveUserGroup(userGroup1); + ServiceContext.UserService.SaveUserGroup(userGroup2); - var user1 = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", userType); - var user2 = ServiceContext.UserService.CreateUserWithIdentity("test2", "test2@test.com", userType); - //adds some allowed sections - user1.AddAllowedSection("test"); - user2.AddAllowedSection("test"); - ServiceContext.UserService.Save(user1); - ServiceContext.UserService.Save(user2); + userGroup1.AddAllowedSection("test"); + userGroup2.AddAllowedSection("test"); + ServiceContext.UserService.SaveUserGroup(userGroup1); + ServiceContext.UserService.SaveUserGroup(userGroup2); //now clear the section from all users - ServiceContext.UserService.DeleteSectionFromAllUsers("test"); + ServiceContext.UserService.DeleteSectionFromAllUserGroups("test"); //assert - var result1 = ServiceContext.UserService.GetUserById((int)user1.Id); - var result2 = ServiceContext.UserService.GetUserById((int)user2.Id); + var result1 = ServiceContext.UserService.GetUserGroupById(userGroup1.Id); + var result2 = ServiceContext.UserService.GetUserGroupById(userGroup2.Id); Assert.IsFalse(result1.AllowedSections.Contains("test")); Assert.IsFalse(result2.AllowedSections.Contains("test")); } [Test] - public void Can_Add_Section_To_All_Users() + public void Can_Add_Section_To_All_UserGroups() { - var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); + var userGroup1 = new UserGroup + { + Id = 1, + Alias = "Group1", + }; + var userGroup2 = new UserGroup + { + Id = 2, + Alias = "Group2", + }; + var userGroup3 = new UserGroup + { + Id = 2, + Alias = "Group3", + }; + ServiceContext.UserService.SaveUserGroup(userGroup1); + ServiceContext.UserService.SaveUserGroup(userGroup2); + ServiceContext.UserService.SaveUserGroup(userGroup3); - var user1 = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", userType); - var user2 = ServiceContext.UserService.CreateUserWithIdentity("test2", "test2@test.com", userType); - var user3 = ServiceContext.UserService.CreateUserWithIdentity("test3", "test3@test.com", userType); - var user4 = ServiceContext.UserService.CreateUserWithIdentity("test4", "test4@test.com", userType); - - //now add the section to specific users - ServiceContext.UserService.AddSectionToAllUsers("test", (int)user1.Id, (int)user2.Id); + //now add the section to specific groups + ServiceContext.UserService.AddSectionToAllUserGroups("test", userGroup1.Id, userGroup2.Id); //assert - var result1 = ServiceContext.UserService.GetUserById((int)user1.Id); - var result2 = ServiceContext.UserService.GetUserById((int)user2.Id); - var result3 = ServiceContext.UserService.GetUserById((int)user3.Id); - var result4 = ServiceContext.UserService.GetUserById((int)user4.Id); + var result1 = ServiceContext.UserService.GetUserGroupById(userGroup1.Id); + var result2 = ServiceContext.UserService.GetUserGroupById(userGroup2.Id); + var result3 = ServiceContext.UserService.GetUserGroupById(userGroup3.Id); Assert.IsTrue(result1.AllowedSections.Contains("test")); Assert.IsTrue(result2.AllowedSections.Contains("test")); Assert.IsFalse(result3.AllowedSections.Contains("test")); - Assert.IsFalse(result4.AllowedSections.Contains("test")); - //now add the section to all users - ServiceContext.UserService.AddSectionToAllUsers("test"); + //now add the section to all groups + ServiceContext.UserService.AddSectionToAllUserGroups("test"); //assert - result1 = ServiceContext.UserService.GetUserById((int)user1.Id); - result2 = ServiceContext.UserService.GetUserById((int)user2.Id); - result3 = ServiceContext.UserService.GetUserById((int)user3.Id); - result4 = ServiceContext.UserService.GetUserById((int)user4.Id); + result1 = ServiceContext.UserService.GetUserGroupById(userGroup1.Id); + result2 = ServiceContext.UserService.GetUserGroupById(userGroup2.Id); + result3 = ServiceContext.UserService.GetUserGroupById(userGroup3.Id); Assert.IsTrue(result1.AllowedSections.Contains("test")); Assert.IsTrue(result2.AllowedSections.Contains("test")); Assert.IsTrue(result3.AllowedSections.Contains("test")); - Assert.IsTrue(result4.AllowedSections.Contains("test")); } [Test] diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs index 15d1ab76fd..3ccdd70966 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs @@ -8,7 +8,7 @@ namespace Umbraco.Tests.TestHelpers.Entities { public class MockedUser { - internal static User CreateUser(IUserType userType = null, string suffix = "", params string[] allowedSections) + internal static User CreateUser(IUserType userType = null, string suffix = "") { if (userType == null) { @@ -29,23 +29,10 @@ namespace Umbraco.Tests.TestHelpers.Entities Username = "TestUser" + suffix }; - if (allowedSections.Any()) - { - foreach (var s in allowedSections) - { - user.AddAllowedSection(s); - } - } - else - { - user.AddAllowedSection("content"); - user.AddAllowedSection("media"); - } - return user; } - internal static IEnumerable CreateUser(IUserType userType, int amount, Action onCreating = null) + internal static IEnumerable CreateMulipleUsers(IUserType userType, int amount, Action onCreating = null) { var list = new List(); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedUserGroup.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedUserGroup.cs new file mode 100644 index 0000000000..f266f55332 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedUserGroup.cs @@ -0,0 +1,17 @@ +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Tests.TestHelpers.Entities +{ + public class MockedUserGroup + { + internal static UserGroup CreateUserGroup(string suffix = "", string[] permissions = null) + { + return new UserGroup() + { + Alias = "testUserGroup" + suffix, + Name = "TestUserGroup" + suffix, + Permissions = permissions ?? new[]{"A", "B", "C"} + }; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TreesAndSections/SectionTests.cs b/src/Umbraco.Tests/TreesAndSections/SectionTests.cs index 67ef50af38..1edd10aec7 100644 --- a/src/Umbraco.Tests/TreesAndSections/SectionTests.cs +++ b/src/Umbraco.Tests/TreesAndSections/SectionTests.cs @@ -70,64 +70,6 @@ namespace Umbraco.Tests.TreesAndSections Assert.IsNull(ApplicationContext.Current.Services.SectionService.GetByAlias(name)); } - /// - /// Creates a new user, assigns the user to existing application, - /// then deletes the user - /// - [Test()] - public void Application_Create_New_User_Assign_Application_And_Delete_User() - { - var name = Guid.NewGuid().ToString("N"); - - //new user - var ut = UserType.GetAllUserTypes().First(); - var user = User.MakeNew(name, name, name, ut); - - //get application - //var app = Application.getAll().First(); - - //assign the app - user.addApplication(Constants.Applications.Content); - //ensure it's added - Assert.AreEqual(1, user.Applications.Count(x => x.alias == Constants.Applications.Content)); - - //delete the user - user.delete(); - - //make sure the assigned applications are gone - Assert.AreEqual(0, user.Applications.Count(x => x.alias == name)); - } - - /// - /// create a new application and assigne an new user and deletes the application making sure the assignments are removed - /// - [Test()] - public void Application_Make_New_Assign_User_And_Delete() - { - var name = Guid.NewGuid().ToString("N"); - - //new user - var ut = UserType.GetAllUserTypes().First(); - var user = User.MakeNew(name, name, name, ut); - - ApplicationContext.Current.Services.SectionService.MakeNew(name, name, "icon.jpg"); - - //check if it exists - var app = ApplicationContext.Current.Services.SectionService.GetByAlias(name); - Assert.IsNotNull(app); - - //assign the app - user.addApplication(app.Alias); - //ensure it's added - Assert.AreEqual(1, user.Applications.Count(x => x.alias == app.Alias)); - - //delete the app - ApplicationContext.Current.Services.SectionService.DeleteSection(app); - - //make sure the assigned applications are gone - Assert.AreEqual(0, user.Applications.Count(x => x.alias == name)); - } - #region Tests to write diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 89053a2d5a..c0f43220d9 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -179,8 +179,10 @@ + + diff --git a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs index 4b78df671d..9634bb164a 100644 --- a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs @@ -100,7 +100,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(9, 1234, new string[]{ "A", "B", "C" }) + new EntityPermission(1234, new string[]{ "A", "B", "C" }) }; userServiceMock.Setup(x => x.GetPermissions(user, 1234)).Returns(permissions); var userService = userServiceMock.Object; @@ -129,7 +129,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(9, 1234, new string[]{ "A", "F", "C" }) + new EntityPermission(1234, new string[]{ "A", "F", "C" }) }; userServiceMock.Setup(x => x.GetPermissions(user, 1234)).Returns(permissions); var userService = userServiceMock.Object; @@ -217,7 +217,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(9, 1234, new string[]{ "A" }) + new EntityPermission(1234, new string[]{ "A" }) }; userServiceMock.Setup(x => x.GetPermissions(user, -1)).Returns(permissions); var userService = userServiceMock.Object; @@ -241,7 +241,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(9, 1234, new string[]{ "A" }) + new EntityPermission(1234, new string[]{ "A" }) }; userServiceMock.Setup(x => x.GetPermissions(user, -1)).Returns(permissions); var userService = userServiceMock.Object; @@ -265,7 +265,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(9, 1234, new string[]{ "A" }) + new EntityPermission(1234, new string[]{ "A" }) }; userServiceMock.Setup(x => x.GetPermissions(user, -20)).Returns(permissions); var userService = userServiceMock.Object; @@ -289,7 +289,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(9, 1234, new string[]{ "A" }) + new EntityPermission(1234, new string[]{ "A" }) }; userServiceMock.Setup(x => x.GetPermissions(user, -20)).Returns(permissions); var userService = userServiceMock.Object; diff --git a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs index 1ebe2fc748..c776924af7 100644 --- a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs @@ -112,10 +112,10 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //we're only assigning 3 nodes browse permissions so that is what we expect as a result var permissions = new List { - new EntityPermission(9, 1, new string[]{ "F" }), - new EntityPermission(9, 2, new string[]{ "F" }), - new EntityPermission(9, 3, new string[]{ "F" }), - new EntityPermission(9, 4, new string[]{ "A" }) + new EntityPermission(1, new string[]{ "F" }), + new EntityPermission(2, new string[]{ "F" }), + new EntityPermission(3, new string[]{ "F" }), + new EntityPermission(4, new string[]{ "A" }) }; userServiceMock.Setup(x => x.GetPermissions(user, ids)).Returns(permissions); var userService = userServiceMock.Object; diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 30563647f8..cc63766afc 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -27,7 +27,7 @@ - + diff --git a/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx b/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx index 815a420f88..fc9270e370 100644 --- a/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx +++ b/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx @@ -27,7 +27,7 @@ - diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 638d124658..b5743df158 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -36,9 +36,9 @@ namespace Umbraco.Web.Cache SectionService.Deleted += ApplicationDeleted; SectionService.New += ApplicationNew; - //bind to user / user type events - UserService.SavedUserType += UserServiceSavedUserType; - UserService.DeletedUserType += UserServiceDeletedUserType; + //bind to user / user group events + UserService.SavedUserGroup += UserServiceSavedUserGroup; + UserService.DeletedUserGroup += UserServiceDeletedUserGroup; UserService.SavedUser += UserServiceSavedUser; UserService.DeletedUser += UserServiceDeletedUser; @@ -349,19 +349,6 @@ namespace Umbraco.Web.Cache } #endregion - #region UserType event handlers - static void UserServiceDeletedUserType(IUserService sender, DeleteEventArgs e) - { - e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserTypeCache(x.Id)); - } - - static void UserServiceSavedUserType(IUserService sender, SaveEventArgs e) - { - e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshUserTypeCache(x.Id)); - } - - #endregion - #region Dictionary event handlers static void LocalizationServiceSavedDictionaryItem(ILocalizationService sender, SaveEventArgs e) @@ -537,7 +524,17 @@ namespace Umbraco.Web.Cache { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserCache(x.Id)); } - + + static void UserServiceSavedUserGroup(IUserService sender, SaveEventArgs e) + { + e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshUserGroupCache(x.Id)); + } + + static void UserServiceDeletedUserGroup(IUserService sender, DeleteEventArgs e) + { + e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserGroupCache(x.Id)); + } + private static void InvalidateCacheForPermissionsChange(UserGroupPermission sender) { if (sender.UserGroup != null) diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index 75715565ac..6956df2374 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -35,7 +35,6 @@ namespace Umbraco.Web.Cache public const string UserCacheRefresherId = "E057AF6D-2EE6-41F4-8045-3694010F0AA6"; public const string UserGroupCacheRefresherId = "45178038-B232-4FE8-AA1A-F2B949C44762"; public const string UserGroupPermissionsCacheRefresherId = "840AB9C5-5C0B-48DB-A77E-29FE4B80CD3A"; - public const string UserTypeCacheRefresherId = "7E707E21-0195-4522-9A3C-658CC1761BD4"; public const string ContentTypeCacheRefresherId = "6902E22C-9C10-483C-91F3-66B7CAE9E2F5"; public const string LanguageCacheRefresherId = "3E0F95D8-0BE5-44B8-8394-2B8750B62654"; public const string DomainCacheRefresherId = "11290A79-4B57-4C99-AD72-7748A3CF38AF"; @@ -58,8 +57,8 @@ namespace Umbraco.Web.Cache public static readonly Guid MediaCacheRefresherGuid = new Guid(MediaCacheRefresherId); public static readonly Guid MacroCacheRefresherGuid = new Guid(MacroCacheRefresherId); public static readonly Guid UserCacheRefresherGuid = new Guid(UserCacheRefresherId); + public static readonly Guid UserGroupCacheRefresherGuid = new Guid(UserGroupCacheRefresherId); public static readonly Guid UserGroupPermissionsCacheRefresherGuid = new Guid(UserGroupPermissionsCacheRefresherId); - public static readonly Guid UserTypeCacheRefresherGuid = new Guid(UserTypeCacheRefresherId); public static readonly Guid ContentTypeCacheRefresherGuid = new Guid(ContentTypeCacheRefresherId); public static readonly Guid LanguageCacheRefresherGuid = new Guid(LanguageCacheRefresherId); public static readonly Guid DomainCacheRefresherGuid = new Guid(DomainCacheRefresherId); diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index f7abb7494d..1050517c5a 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -41,25 +41,6 @@ namespace Umbraco.Web.Cache #endregion - #region User type cache - - public static void RemoveUserTypeCache(this DistributedCache dc, int userTypeId) - { - dc.Remove(DistributedCache.UserTypeCacheRefresherGuid, userTypeId); - } - - public static void RefreshUserTypeCache(this DistributedCache dc, int userTypeId) - { - dc.Refresh(DistributedCache.UserTypeCacheRefresherGuid, userTypeId); - } - - public static void RefreshAllUserTypeCache(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.UserTypeCacheRefresherGuid); - } - - #endregion - #region User cache public static void RemoveUserCache(this DistributedCache dc, int userId) @@ -75,7 +56,26 @@ namespace Umbraco.Web.Cache public static void RefreshAllUserCache(this DistributedCache dc) { dc.RefreshAll(DistributedCache.UserCacheRefresherGuid); - } + } + + #endregion + + #region User group cache + + public static void RemoveUserGroupCache(this DistributedCache dc, int userId) + { + dc.Remove(DistributedCache.UserGroupCacheRefresherGuid, userId); + } + + public static void RefreshUserGroupCache(this DistributedCache dc, int userId) + { + dc.Refresh(DistributedCache.UserGroupCacheRefresherGuid, userId); + } + + public static void RefreshAllUserGroupCache(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.UserGroupCacheRefresherGuid); + } #endregion diff --git a/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs deleted file mode 100644 index ca3f93c068..0000000000 --- a/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Models.Membership; - -using Umbraco.Core.Persistence.Repositories; - -namespace Umbraco.Web.Cache -{ - /// - /// Handles User type cache invalidation/refreshing - /// - public sealed class UserTypeCacheRefresher : CacheRefresherBase - { - protected override UserTypeCacheRefresher Instance - { - get { return this; } - } - - public override Guid UniqueIdentifier - { - get { return Guid.Parse(DistributedCache.UserTypeCacheRefresherId); } - } - - public override string Name - { - get { return "User type cache refresher"; } - } - - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - var userTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); - if (userTypeCache) - userTypeCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - base.Refresh(id); - } - - public override void Remove(int id) - { - var userTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); - if (userTypeCache) - userTypeCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - base.Remove(id); - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 199ffd9bed..bc596a8a0a 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -604,60 +604,49 @@ namespace Umbraco.Web.Editors } else { - var defaultUserType = autoLinkOptions.GetDefaultUserType(UmbracoContext, loginInfo); - var userType = Services.UserService.GetUserTypeByAlias(defaultUserType); - if (userType == null) + if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); + if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); + + var autoLinkUser = new BackOfficeIdentityUser { - ViewData[TokenExternalSignInError] = new[] { "Could not auto-link this account, the specified User Type does not exist: " + defaultUserType }; + Email = loginInfo.Email, + Name = loginInfo.ExternalIdentity.Name, + AllowedSections = autoLinkOptions.GetDefaultAllowedSections(UmbracoContext, loginInfo), + Culture = autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo), + UserName = loginInfo.Email + }; + + //call the callback if one is assigned + if (autoLinkOptions.OnAutoLinking != null) + { + autoLinkOptions.OnAutoLinking(autoLinkUser, loginInfo); + } + + var userCreationResult = await UserManager.CreateAsync(autoLinkUser); + + if (userCreationResult.Succeeded == false) + { + ViewData[TokenExternalSignInError] = userCreationResult.Errors; } else { - - if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); - if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); - - var autoLinkUser = new BackOfficeIdentityUser() + var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); + if (linkResult.Succeeded == false) { - Email = loginInfo.Email, - Name = loginInfo.ExternalIdentity.Name, - UserTypeAlias = userType.Alias, - AllowedSections = autoLinkOptions.GetDefaultAllowedSections(UmbracoContext, loginInfo), - Culture = autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo), - UserName = loginInfo.Email - }; + ViewData[TokenExternalSignInError] = linkResult.Errors; - //call the callback if one is assigned - if (autoLinkOptions.OnAutoLinking != null) - { - autoLinkOptions.OnAutoLinking(autoLinkUser, loginInfo); - } - - var userCreationResult = await UserManager.CreateAsync(autoLinkUser); - - if (userCreationResult.Succeeded == false) - { - ViewData[TokenExternalSignInError] = userCreationResult.Errors; + //If this fails, we should really delete the user since it will be in an inconsistent state! + var deleteResult = await UserManager.DeleteAsync(autoLinkUser); + if (deleteResult.Succeeded == false) + { + //DOH! ... this isn't good, combine all errors to be shown + ViewData[TokenExternalSignInError] = linkResult.Errors.Concat(deleteResult.Errors); + } } else { - var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); - if (linkResult.Succeeded == false) - { - ViewData[TokenExternalSignInError] = linkResult.Errors; - - //If this fails, we should really delete the user since it will be in an inconsistent state! - var deleteResult = await UserManager.DeleteAsync(autoLinkUser); - if (deleteResult.Succeeded == false) - { - //DOH! ... this isn't good, combine all errors to be shown - ViewData[TokenExternalSignInError] = linkResult.Errors.Concat(deleteResult.Errors); - } - } - else - { - //sign in - await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); - } + //sign in + await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); } } } diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index d668399e83..e4c94965b7 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -37,13 +37,12 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.InternalServerError); var user = Security.CurrentUser; - var userType = user.UserType.Alias; var allowedSections = string.Join(",", user.AllowedSections); var language = user.Language; var version = UmbracoVersion.GetSemanticVersion().ToSemanticString(); - var url = string.Format(baseUrl + "{0}?section={0}&type={1}&allowed={2}&lang={3}&version={4}", section, userType, allowedSections, language, version); - var key = "umbraco-dynamic-dashboard-" + userType + language + allowedSections.Replace(",", "-") + section; + var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); + var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = ApplicationContext.ApplicationCache.RuntimeCache.GetCacheItem(key); var result = new JObject(); diff --git a/src/Umbraco.Web/Editors/DashboardSecurity.cs b/src/Umbraco.Web/Editors/DashboardSecurity.cs index a8f5e44ef2..879fd468e0 100644 --- a/src/Umbraco.Web/Editors/DashboardSecurity.cs +++ b/src/Umbraco.Web/Editors/DashboardSecurity.cs @@ -76,33 +76,6 @@ namespace Umbraco.Web.Editors } } - //Check if this item as any grant arguments, if so check if the user is one of the user types approved, if so they will - // be allowed to see it (so far) - if (grantedTypes.Any()) - { - var allApprovedUserTypes = grantedTypes.SelectMany(g => g.Value.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - if (allApprovedUserTypes.InvariantContains(user.UserType.Alias)) - { - allowedSoFar = true; - } - } - else - { - //if there are not explicit grant types then everyone is allowed so far and we'll only disallow on a deny basis - allowedSoFar = true; - } - - //Check if this item as any deny arguments, if so check if the user is one of the user types approved, if so they will - // be denied to see it no matter what - if (denyTypes.Any()) - { - var allDeniedUserTypes = denyTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - if (allDeniedUserTypes.InvariantContains(user.UserType.Alias)) - { - allowedSoFar = false; - } - } - return allowedSoFar; } } diff --git a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs b/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs index 8ccfd6dde7..9fb7a63c9f 100644 --- a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs @@ -72,8 +72,7 @@ namespace Umbraco.Web.Editors var userIsAdmin = Umbraco.UmbracoContext.Security.CurrentUser.IsAdmin(); if (userIsAdmin == false) { - var errorMessage = string.Format("User of type {0} is not allowed to toggle the URL tracker", - Umbraco.UmbracoContext.Security.CurrentUser.UserType.Alias); + var errorMessage = "User is not a member of the administrators group and so is not allowed to toggle the URL tracker"; LogHelper.Debug(errorMessage); throw new UserAuthorizationException(errorMessage); } diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index d27736576f..ada24ee38c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -21,10 +21,6 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "emailHash")] public string EmailHash { get; set; } - [DataMember(Name = "userType", IsRequired = true)] - [Required] - public string UserType { get; set; } - /// /// Gets/sets the number of seconds for the user's auth ticket to expire /// diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 72093b19a8..213abc5afc 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models.Mapping; @@ -17,7 +18,6 @@ namespace Umbraco.Web.Models.Mapping { config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => GetIntId(user.Id))) - .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) @@ -28,7 +28,6 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => user.Id)) - .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserTypeAlias)) .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture)) @@ -46,7 +45,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) - .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => new[] {user.UserType.Alias})) + .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Groups.Select(x => x.Name))) .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId)) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index f3b0eaeb5f..3cff7344ad 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -229,9 +229,6 @@ namespace Umbraco.Web.Security //if we already have a user object in Umbraco we don't need to do anything, otherwise we need to create a mapped Umbraco account. if (user != null) return user; - //we need to create an Umbraco IUser of a 'writer' type with access to only content - this was how v6 operates. - var writer = _applicationContext.Services.UserService.GetUserTypeByAlias("writer"); - var email = membershipUser.Email; if (email.IsNullOrWhiteSpace()) { @@ -239,7 +236,7 @@ namespace Umbraco.Web.Security email = Guid.NewGuid().ToString("N") + "@example.com"; } - user = new Core.Models.Membership.User(writer) + user = new Core.Models.Membership.User { Email = email, Language = GlobalSettings.DefaultUILanguage, diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a324e39af4..c2b5665f4c 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -476,7 +476,6 @@ - @@ -1856,18 +1855,10 @@ - XmlTree.xsd - - EditUserType.aspx - ASPXCodeBehind - - - EditUserType.aspx - NodePermissions.ascx ASPXCodeBehind @@ -1886,7 +1877,6 @@ PermissionsHandler.asmx - CacheRefresher.asmx @@ -2151,9 +2141,6 @@ ASPXCodeBehind - - ASPXCodeBehind - Form diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserTypes.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserTypes.cs deleted file mode 100644 index 54dd21738b..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserTypes.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using umbraco.BusinessLogic; -using System.Collections.Generic; -using umbraco.businesslogic; -using Umbraco.Core; - -namespace umbraco.cms.presentation.Trees -{ - [Tree(Constants.Applications.Users, "userTypes", "User Types", sortOrder: 1)] - public class UserTypes : BaseTree - { - - public UserTypes(string application) : base(application) { } - - public override void RenderJS(ref System.Text.StringBuilder Javascript) - { - Javascript.Append( - @" -function openUserTypes(id) { - UmbClientMgr.contentFrame('users/EditUserType.aspx?id=' + id); -} -"); - } - - public override void Render(ref XmlTree tree) - { - List userTypes = UserType.GetAllUserTypes(); - foreach (UserType userType in userTypes) - { - if (userType.Id > 1) //don't show the admin user type, they should always have full permissions - { - XmlTreeNode node = XmlTreeNode.Create(this); - node.NodeID = userType.Id.ToString(); - node.Action = string.Format("javascript:openUserTypes({0})", userType.Id.ToString()); - node.Icon = "icon-users"; - node.Text = userType.Name; - - OnBeforeNodeRender(ref tree, ref node, EventArgs.Empty); - if (node != null) - { - tree.Add(node); - OnAfterNodeRender(ref tree, ref node, EventArgs.Empty); - } - } - } - } - - protected override void CreateRootNode(ref XmlTreeNode rootNode) - { - rootNode.Text = ui.Text("user", "userTypes"); - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/RelationTypesWebService.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/RelationTypesWebService.asmx.cs index 2b775ed797..2b5965d2f3 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/RelationTypesWebService.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/RelationTypesWebService.asmx.cs @@ -1,10 +1,11 @@ using System.Web.Services; using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Web; namespace umbraco.cms.presentation.developer.RelationTypes { - /// + /// /// Webservice to delete relation types, this allows deletion via a javacscript call hooked into the tree UI /// [WebService(Namespace = "http://tempuri.org/")] @@ -22,7 +23,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes { var user = UmbracoContext.Current.Security.CurrentUser; - if (user.UserType.Name == "Administrators") + if (user.IsAdmin()) { var relationService = ApplicationContext.Current.Services.RelationService; var relationType = relationService.GetRelationTypeById(relationTypeId); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sendToTranslation.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sendToTranslation.aspx.cs index c02baaaa7b..a310764498 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sendToTranslation.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sendToTranslation.aspx.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Data; using System.Configuration; using System.Collections; @@ -70,7 +71,7 @@ namespace umbraco.presentation.dialogs // Translators foreach (var u in BusinessLogic.User.getAll()) - if (u.UserType.Alias.ToLower() == "translator" || UserHasTranslatePermission(u, _currentPage)) + if (u.GetGroups().Select(x => x.ToLower()).Contains("translators") || UserHasTranslatePermission(u, _currentPage)) translator.Items.Add(new ListItem(u.Name, u.Id.ToString())); if (translator.Items.Count == 0) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index 510a526a0e..83c30ddc25 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -93,19 +93,6 @@ namespace umbraco.cms.presentation.user throw new Exception("Admin users can only be edited by admins"); } - // Populate usertype list - foreach (UserType ut in UserType.getAll) - { - if (CurrentUser.IsAdmin() || ut.Alias != "admin") - { - ListItem li = new ListItem(ui.Text("user", ut.Name.ToLower(), UmbracoUser), ut.Id.ToString()); - if (ut.Id == u.UserType.Id) - li.Selected = true; - - userType.Items.Add(li); - } - } - var userCulture = UserExtensions.GetUserCulture(u.Language, Services.TextService); // Populate ui language lsit @@ -467,7 +454,6 @@ namespace umbraco.cms.presentation.user // ok since the membership provider might be storing these details someplace totally different! But we want to keep our UI in sync. u.Name = uname.Text.Trim(); u.Language = userLanguage.SelectedValue; - u.UserType = UserType.GetUserType(int.Parse(userType.SelectedValue)); u.Email = email.Text.Trim(); u.LoginName = lname.Text; u.Disabled = Disabled.Checked; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx deleted file mode 100644 index d8671f0596..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx +++ /dev/null @@ -1,27 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EditUserType.aspx.cs" MasterPageFile="../masterpages/umbracoPage.Master" - Inherits="umbraco.cms.presentation.user.EditUserType" %> - -<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - - - - - - - - - - - - - - - diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx.cs deleted file mode 100644 index b6796e45fa..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Collections; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; -using umbraco.BasePages; -using System.Collections.Generic; -using umbraco.interfaces; -using umbraco.BusinessLogic.Actions; -using umbraco.BusinessLogic; -using umbraco.uicontrols; -using umbraco.cms.presentation.Trees; -using Umbraco.Core.IO; - -namespace umbraco.cms.presentation.user -{ - public partial class EditUserType : UmbracoEnsuredPage - { - public EditUserType() - { - CurrentApp = BusinessLogic.DefaultApps.users.ToString(); - } - protected void Page_Load(object sender, EventArgs e) - { - pnlUmbraco.Text = umbraco.ui.Text("usertype", base.getUser()); - - var save = pnlUmbraco.Menu.NewButton(); - save.Click += save_Click; - save.ID = "save"; - save.ToolTip = ui.Text("save"); - save.Text = ui.Text("save"); - - pp_alias.Text = umbraco.ui.Text("usertype", base.getUser()) + " " + umbraco.ui.Text("alias", base.getUser()); - pp_name.Text = umbraco.ui.Text("usertype", base.getUser()) + " " + umbraco.ui.Text("name", base.getUser()); - - pp_rights.Text = umbraco.ui.Text("default", base.getUser()) + " " + umbraco.ui.Text("rights", base.getUser()); - - //ensure we have a query string - if (string.IsNullOrEmpty(Request.QueryString["id"])) - return; - //ensuer it is an integer - if (!int.TryParse(Request.QueryString["id"], out m_userTypeID)) - return; - - if (!IsPostBack) - { - BindActions(); - - ClientTools - .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) - .SyncTree(m_userTypeID.ToString(), false); - } - - } - - void save_Click(object sender, EventArgs e) - { - UserType userType = CurrentUserType; - userType.Name = txtUserTypeName.Text; - string actions = ""; - - foreach (ListItem li in cbl_rights.Items) { - if (li.Selected) - actions += li.Value; - } - - userType.DefaultPermissions = actions; - userType.Save(); - - ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "editUserTypeSaved", base.getUser()), ""); - } - - protected List CurrentUserTypeActions - { - get - { - if (m_userTypeActions == null) - m_userTypeActions = umbraco.BusinessLogic.Actions.Action.FromString(CurrentUserType.DefaultPermissions); - return m_userTypeActions; - } - } - - protected UserType CurrentUserType - { - get - { - if (m_userType == null) - m_userType = UserType.GetUserType(m_userTypeID); - return m_userType; - } - } - private UserType m_userType; - private List m_userTypeActions; - private int m_userTypeID; - - private void BindActions() - { - lblUserTypeAlias.Text = CurrentUserType.Alias; - txtUserTypeName.Text = CurrentUserType.Name; - hidUserTypeID.Value = CurrentUserType.Id.ToString(); - - foreach (IAction ai in global::umbraco.BusinessLogic.Actions.Action.GetPermissionAssignable()) { - - ListItem li = new ListItem(umbraco.ui.Text(ai.Alias, base.getUser()), ai.Letter.ToString()); - - if(CurrentUserTypeActions.Contains(ai)) - li.Selected = true; - - cbl_rights.Items.Add(li); - } - } - - - - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx.designer.cs deleted file mode 100644 index ac1fddb9ec..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserType.aspx.designer.cs +++ /dev/null @@ -1,105 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.cms.presentation.user { - - - public partial class EditUserType { - - /// - /// pnlUmbraco control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.UmbracoPanel pnlUmbraco; - - /// - /// pnl1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pnl1; - - /// - /// hidUserTypeID control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.HiddenField hidUserTypeID; - - /// - /// pp_name control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_name; - - /// - /// txtUserTypeName control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox txtUserTypeName; - - /// - /// pp_alias control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_alias; - - /// - /// lblUserTypeAlias control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Label lblUserTypeAlias; - - /// - /// pnl2 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pnl2; - - /// - /// pp_rights control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_rights; - - /// - /// cbl_rights control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList cbl_rights; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserTypeTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserTypeTasks.cs deleted file mode 100644 index 0a8e02341a..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserTypeTasks.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; -using Umbraco.Web.UI; -using umbraco.interfaces; -using umbraco.BusinessLogic; - -namespace umbraco.cms.presentation.user -{ - public class UserTypeTasks : LegacyDialogTask - { - public override bool PerformSave() - { - try - { - var u = UserType.MakeNew(Alias, "", Alias); - _returnUrl = string.Format("users/EditUserType.aspx?id={0}", u.Id); - return true; - } - catch - { - return false; - } - } - - public override bool PerformDelete() - { - var userType = UserType.GetUserType(ParentID); - if (userType == null) - return false; - userType.Delete(); - return true; - } - - private string _returnUrl = ""; - public override string ReturnUrl - { - get { return _returnUrl; } - } - - public override string AssignedApp - { - get { return DefaultApps.users.ToString(); } - } - } -} diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index 8e7f1f47e1..bf026a5c62 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -293,7 +293,7 @@ namespace umbraco.BasePages AllowedApplications = u.GetApplications().Select(x => x.alias).ToArray(), RealName = u.Name, //currently we only have one user type! - Roles = new[] { u.UserType.Alias }, + Roles = u.GetGroups(), StartContentNode = u.StartNodeId, StartMediaNode = u.StartMediaId, Username = u.LoginName, diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index e8ed675998..f6096306c7 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -313,24 +313,6 @@ namespace umbraco.BusinessLogic return (tmp != null); } - /// - /// Gets or sets the type of the user. - /// - /// The type of the user. - public UserType UserType - { - get - { - if (_lazyId.HasValue) SetupUser(_lazyId.Value); - return new UserType(UserEntity.UserType); - } - set - { - UserEntity.UserType = value.UserTypeItem; - } - } - - /// /// Gets all users /// @@ -447,10 +429,9 @@ namespace umbraco.BusinessLogic /// The full name. /// The login name. /// The password. - /// The user type. - public static User MakeNew(string name, string lname, string passw, UserType ut) + public static User MakeNew(string name, string lname, string passw) { - var user = new Umbraco.Core.Models.Membership.User(name, "", lname, passw, ut.UserTypeItem); + var user = new Umbraco.Core.Models.Membership.User(name, "", lname, passw); ApplicationContext.Current.Services.UserService.Save(user); var u = new User(user); @@ -467,10 +448,9 @@ namespace umbraco.BusinessLogic /// The lname. /// The passw. /// The email. - /// The ut. - public static User MakeNew(string name, string lname, string passw, string email, UserType ut) + public static User MakeNew(string name, string lname, string passw, string email) { - var user = new Umbraco.Core.Models.Membership.User(name, email, lname, passw, ut.UserTypeItem); + var user = new Umbraco.Core.Models.Membership.User(name, email, lname, passw); ApplicationContext.Current.Services.UserService.Save(user); var u = new User(user); @@ -487,8 +467,7 @@ namespace umbraco.BusinessLogic /// The name. /// The lname. /// The email. - /// The ut. - public static void Update(int id, string name, string lname, string email, UserType ut) + public static void Update(int id, string name, string lname, string email) { if (EnsureUniqueLoginName(lname, GetUser(id)) == false) throw new Exception(String.Format("A user with the login '{0}' already exists", lname)); @@ -498,11 +477,10 @@ namespace umbraco.BusinessLogic found.Name = name; found.Username = lname; found.Email = email; - found.UserType = ut.UserTypeItem; ApplicationContext.Current.Services.UserService.Save(found); } - public static void Update(int id, string name, string lname, string email, bool disabled, bool noConsole, UserType ut) + public static void Update(int id, string name, string lname, string email, bool disabled, bool noConsole) { if (EnsureUniqueLoginName(lname, GetUser(id)) == false) throw new Exception(String.Format("A user with the login '{0}' already exists", lname)); @@ -512,7 +490,6 @@ namespace umbraco.BusinessLogic found.Name = name; found.Username = lname; found.Email = email; - found.UserType = ut.UserTypeItem; found.IsApproved = disabled == false; found.IsLockedOut = noConsole; ApplicationContext.Current.Services.UserService.Save(found); @@ -681,7 +658,7 @@ namespace umbraco.BusinessLogic } /// - /// Adds a group to the list groups for the user, ensure to call Save() afterwords + /// Adds a group to the list of groups for the user, ensure to call Save() afterwords /// public void AddGroup(int groupId, string groupName) { @@ -693,6 +670,12 @@ namespace umbraco.BusinessLogic }); } + public string[] GetGroups() + { + if (_lazyId.HasValue) SetupUser(_lazyId.Value); + return UserEntity.Groups.Select(x => x.Name).ToArray(); + } + /// /// Gets or sets a value indicating whether the user has access to the Umbraco back end. diff --git a/src/umbraco.businesslogic/UserType.cs b/src/umbraco.businesslogic/UserType.cs deleted file mode 100644 index b7bc2b9675..0000000000 --- a/src/umbraco.businesslogic/UserType.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Web; -using System.Web.Caching; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Events; -using umbraco.DataLayer; - -namespace umbraco.BusinessLogic -{ - /// - /// Represents a umbraco Usertype - /// - [Obsolete("Use the UserService instead")] - public class UserType - { - - internal Umbraco.Core.Models.Membership.IUserType UserTypeItem; - - /// - /// Creates a new empty instance of a UserType - /// - public UserType() - { - UserTypeItem = new Umbraco.Core.Models.Membership.UserType(); - } - - internal UserType(Umbraco.Core.Models.Membership.IUserType userType) - { - UserTypeItem = userType; - } - - /// - /// Creates a new instance of a UserType and attempts to - /// load it's values from the database cache. - /// - /// - /// If the UserType is not found in the existing ID list, then this object - /// will remain an empty object - /// - /// The UserType id to find - public UserType(int id) - { - this.LoadByPrimaryKey(id); - } - - /// - /// Initializes a new instance of the class. - /// - /// The user type id. - /// The name. - public UserType(int id, string name) - { - UserTypeItem = new Umbraco.Core.Models.Membership.UserType(); - UserTypeItem.Id = id; - UserTypeItem.Name = name; - } - - /// - /// Creates a new instance of UserType with all parameters - /// - /// - /// - /// - /// - public UserType(int id, string name, string defaultPermissions, string alias) - { - UserTypeItem = new Umbraco.Core.Models.Membership.UserType(); - UserTypeItem.Id = id; - UserTypeItem.Name = name; - UserTypeItem.Alias = alias; - UserTypeItem.Permissions = defaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// The cache storage for all user types - /// - private static List UserTypes - { - get - { - return ApplicationContext.Current.Services.UserService.GetAllUserTypes() - .Select(x => new UserType(x)) - .ToList(); - } - } - - #region Public Properties - /// - /// Gets or sets the user type alias. - /// - public string Alias - { - get { return UserTypeItem.Alias; } - set { UserTypeItem.Alias = value; } - } - - /// - /// Gets the name of the user type. - /// - public string Name - { - get { return UserTypeItem.Name; } - set { UserTypeItem.Name = value; } - } - - /// - /// Gets the id the user type - /// - public int Id - { - get { return UserTypeItem.Id; } - } - - /// - /// Gets the default permissions of the user type - /// - public string DefaultPermissions - { - get { return UserTypeItem.Permissions == null ? string.Empty : string.Join("", UserTypeItem.Permissions); } - set { UserTypeItem.Permissions = value.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); } - } - - /// - /// Returns an array of UserTypes - /// - [Obsolete("Use the GetAll method instead")] - public static UserType[] getAll - { - get { return GetAllUserTypes().ToArray(); } - } - #endregion - - /// - /// Saves this instance. - /// - public void Save() - { - //ensure that this object has an ID specified (it exists in the database) - if (UserTypeItem.HasIdentity == false) - throw new Exception("The current UserType object does not exist in the database. New UserTypes should be created with the MakeNew method"); - - ApplicationContext.Current.Services.UserService.SaveUserType(UserTypeItem); - - //raise event - OnUpdated(this, new EventArgs()); - } - - /// - /// Deletes this instance. - /// - public void Delete() - { - //ensure that this object has an ID specified (it exists in the database) - if (UserTypeItem.HasIdentity == false) - throw new Exception("The current UserType object does not exist in the database. New UserTypes should be created with the MakeNew method"); - - ApplicationContext.Current.Services.UserService.DeleteUserType(UserTypeItem); - - //raise event - OnDeleted(this, new EventArgs()); - } - - /// - /// Load the data for the current UserType by it's id - /// - /// - /// Returns true if the UserType id was found - /// and the data was loaded, false if it wasn't - public bool LoadByPrimaryKey(int id) - { - UserTypeItem = ApplicationContext.Current.Services.UserService.GetUserTypeById(id); - return UserTypeItem != null; - } - - /// - /// Creates a new user type - /// - /// - /// - /// - public static UserType MakeNew(string name, string defaultPermissions, string alias) - { - //ensure that the current alias does not exist - //get the id for the new user type - var existing = UserTypes.Find(ut => (ut.Alias == alias)); - - if (existing != null) - throw new Exception("The UserType alias specified already exists"); - - var userType = new Umbraco.Core.Models.Membership.UserType - { - Alias = alias, - Name = name, - Permissions = defaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)) - }; - ApplicationContext.Current.Services.UserService.SaveUserType(userType); - - var legacy = new UserType(userType); - - //raise event - OnNew(legacy, new EventArgs()); - - return legacy; - } - - /// - /// Gets the user type with the specied ID - /// - /// The id. - /// - public static UserType GetUserType(int id) - { - return UserTypes.Find(ut => (ut.Id == id)); - } - - /// - /// Returns all UserType's - /// - /// - public static List GetAllUserTypes() - { - return UserTypes; - } - - internal static event TypedEventHandler New; - private static void OnNew(UserType userType, EventArgs args) - { - if (New != null) - { - New(userType, args); - } - } - - internal static event TypedEventHandler Deleted; - private static void OnDeleted(UserType userType, EventArgs args) - { - if (Deleted != null) - { - Deleted(userType, args); - } - } - - internal static event TypedEventHandler Updated; - private static void OnUpdated(UserType userType, EventArgs args) - { - if (Updated != null) - { - Updated(userType, args); - } - } - - } -} \ No newline at end of file diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index 0d6aae1b71..87de399469 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -242,9 +242,6 @@ Code - - Code - Code diff --git a/src/umbraco.editorControls/tinyMCE3/TinyMCE.cs b/src/umbraco.editorControls/tinyMCE3/TinyMCE.cs index fcae44b113..cf6fba9e43 100644 --- a/src/umbraco.editorControls/tinyMCE3/TinyMCE.cs +++ b/src/umbraco.editorControls/tinyMCE3/TinyMCE.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Linq; using System.Text.RegularExpressions; using System.Web; using System.Web.UI; @@ -136,8 +137,8 @@ namespace umbraco.editorControls.tinyMCE3 config.Add("plugins", _plugins); // Check advanced settings - if (UmbracoEnsuredPage.CurrentUser != null && ("," + _advancedUsers + ",").IndexOf("," + UmbracoEnsuredPage.CurrentUser.UserType.Id + ",") > - -1) + var advancedUserGroupNames = _advancedUsers.Split(','); + if (UmbracoEnsuredPage.CurrentUser != null && advancedUserGroupNames.Intersect(UmbracoEnsuredPage.CurrentUser.GetGroups()).Any()) config.Add("umbraco_advancedMode", "true"); else config.Add("umbraco_advancedMode", "false"); diff --git a/src/umbraco.editorControls/tinymce/tinyMCEPreValueConfigurator.cs b/src/umbraco.editorControls/tinymce/tinyMCEPreValueConfigurator.cs index 533a961416..fe6505be54 100644 --- a/src/umbraco.editorControls/tinymce/tinyMCEPreValueConfigurator.cs +++ b/src/umbraco.editorControls/tinymce/tinyMCEPreValueConfigurator.cs @@ -1,15 +1,15 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Collections; using System.Web.UI; using System.Web.UI.WebControls; - using umbraco.BusinessLogic; using umbraco.DataLayer; +using Umbraco.Core; namespace umbraco.editorControls.tinymce { + + [Obsolete("IDataType and all other references to the legacy property editors are no longer used this will be removed from the codebase in future versions")] public class tinyMCEPreValueConfigurator : System.Web.UI.WebControls.PlaceHolder, interfaces.IDataPrevalue { @@ -178,10 +178,11 @@ namespace umbraco.editorControls.tinymce } // add users - foreach (BusinessLogic.UserType ut in BusinessLogic.UserType.getAll) + var userService = ApplicationContext.Current.Services.UserService; + foreach (var ug in userService.GetAllUserGroups()) { - ListItem li = new ListItem(ut.Name, ut.Id.ToString()); - if (("," + _advancedUsers + ",").IndexOf("," + ut.Id.ToString() + ",") > -1) + ListItem li = new ListItem(ug.Name, ug.Id.ToString()); + if (("," + _advancedUsers + ",").IndexOf("," + ug.Id + ",") > -1) li.Selected = true; _advancedUsersList.Items.Add(li); diff --git a/src/umbraco.providers/UsersMembershipProvider.cs b/src/umbraco.providers/UsersMembershipProvider.cs index 93f5327cbb..39285449a9 100644 --- a/src/umbraco.providers/UsersMembershipProvider.cs +++ b/src/umbraco.providers/UsersMembershipProvider.cs @@ -137,17 +137,11 @@ namespace umbraco.providers try { // Get the usertype of the current user - var ut = UserType.GetUserType(1); - if (BasePages.UmbracoEnsuredPage.CurrentUser != null) - { - ut = BasePages.UmbracoEnsuredPage.CurrentUser.UserType; - } - //ensure the password is encrypted/hashed string salt; var encodedPass = EncryptOrHashNewPassword(password, out salt); - User.MakeNew(username, username, FormatPasswordForStorage(encodedPass, salt), email, ut); + User.MakeNew(username, username, FormatPasswordForStorage(encodedPass, salt), email); status = MembershipCreateStatus.Success; } @@ -468,8 +462,8 @@ namespace umbraco.providers } else { - //This keeps compatibility - even though this logic to update name and user type should not exist here - User.Update(m.Id, typedUser.FullName.Trim(), typedUser.UserName, typedUser.Email, user.IsApproved == false, user.IsLockedOut, typedUser.UserType); + //This keeps compatibility - even though this logic to update name should not exist here + User.Update(m.Id, typedUser.FullName.Trim(), typedUser.UserName, typedUser.Email, user.IsApproved == false, user.IsLockedOut); } m.Save(); @@ -562,7 +556,7 @@ namespace umbraco.providers return new UsersMembershipUser(base.Name, user.LoginName, user.Id, user.Email, string.Empty, string.Empty, true, user.Disabled, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, - DateTime.Now, user.Name, user.Language, user.UserType); + DateTime.Now, user.Name, user.Language); } #endregion diff --git a/src/umbraco.providers/UsersMembershipUser.cs b/src/umbraco.providers/UsersMembershipUser.cs index dd406662dc..ffb2d75ae9 100644 --- a/src/umbraco.providers/UsersMembershipUser.cs +++ b/src/umbraco.providers/UsersMembershipUser.cs @@ -38,17 +38,7 @@ namespace umbraco.providers get { return _Language; } set { _Language = value; } } - private UserType _UserType; - /// - /// Gets or sets the type of the user. - /// - /// The type of the user. - public UserType UserType - { - get { return _UserType; } - set { _UserType = value; } - } #endregion #region Constructors @@ -78,16 +68,14 @@ namespace umbraco.providers /// The last lockout date. /// The full name. /// The language. - /// Type of the user. public UsersMembershipUser(string providerName, string name, object providerUserKey, string email, string passwordQuestion, string comment, bool isApproved, bool isLockedOut, DateTime creationDate, DateTime lastLoginDate, DateTime lastActivityDate, DateTime lastPasswordChangedDate, - DateTime lastLockoutDate, string fullName, string language, UserType userType ) + DateTime lastLockoutDate, string fullName, string language) : base( providerName, name, providerUserKey, email, passwordQuestion, comment, isApproved, isLockedOut, creationDate, lastLoginDate, lastActivityDate, lastPasswordChangedDate, lastLockoutDate) { _FullName = fullName; - _UserType = userType; _Language = language; } #endregion From 92010adce6bdfc7c1555d528437766a2498d81e1 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 29 Oct 2016 08:28:57 +0200 Subject: [PATCH 006/714] Permission checks on nodes for users and groups --- src/Umbraco.Core/Models/Membership/IUser.cs | 9 - src/Umbraco.Core/Models/Membership/User.cs | 11 -- .../AddUserGroupTables.cs | 10 + .../Repositories/PermissionRepository.cs | 1 + .../Repositories/UserRepository.cs | 3 - src/Umbraco.Core/Services/IUserService.cs | 8 - src/Umbraco.Core/Services/SectionService.cs | 1 - src/Umbraco.Core/Services/UserService.cs | 183 +++++++----------- src/Umbraco.Tests/Models/UserTests.cs | 5 - .../Repositories/UserRepositoryTest.cs | 23 --- .../Services/UserServiceTests.cs | 1 - .../TestHelpers/Entities/MockedUser.cs | 1 - src/umbraco.businesslogic/UserGroup.cs | 8 +- 13 files changed, 89 insertions(+), 175 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 1fb82116f9..171a9f0c99 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -31,15 +31,6 @@ namespace Umbraco.Core.Models.Membership void SetGroupsLoaded(); - //TODO: This should be a private set - /// - /// The default permission set for the user - /// - /// - /// Currently in umbraco each permission is a single char but with an Enumerable{string} collection this allows for flexible changes to this in the future - /// - IEnumerable DefaultPermissions { get; set; } - IEnumerable AllowedSections { get; } /// diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index d6d02df8f3..8dd1f31654 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -62,8 +62,6 @@ namespace Umbraco.Core.Models.Membership private DateTime _lastLoginDate; private DateTime _lastLockoutDate; - private IEnumerable _defaultPermissions; - private bool _defaultToLiveEditing; private static readonly Lazy Ps = new Lazy(); @@ -259,14 +257,6 @@ namespace Umbraco.Core.Models.Membership set { SetPropertyValueAndDetectChanges(value, ref _language, Ps.Value.LanguageSelector); } } - //TODO: This should be a private set - [DataMember] - public IEnumerable DefaultPermissions - { - get { return _defaultPermissions;} - set { _defaultPermissions = value; } - } - [IgnoreDataMember] internal bool DefaultToLiveEditing { @@ -318,7 +308,6 @@ namespace Umbraco.Core.Models.Membership clone.DisableChangeTracking(); //need to create new collections otherwise they'll get copied by ref clone._groupCollection = new List(_groupCollection.ToList()); - clone._defaultPermissions = new List(_defaultPermissions.ToList()); //re-create the event handler //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs index dae7505462..603517bbe3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs @@ -100,6 +100,16 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero ) FROM umbracoUser u"); + // Add the built-in administrator account to all apps + Execute.Sql(@"INSERT INTO umbracoUserGroup2app (userGroupId,app) + SELECT ug.id, app + FROM umbracoUserGroup ug + INNER JOIN umbracoUser2UserGroup u2ug ON u2ug.userGroupId = ug.id + INNER JOIN umbracoUser u ON u.id = u2ug.userId + INNER JOIN umbracoUser2app u2a ON u2a.[user] = u.id + WHERE u.id = 0"); + + // Rename some groups for consistency (plural form) Execute.Sql("UPDATE umbracoUserGroup SET userGroupName = 'Writers' WHERE userGroupName = 'Writer'"); Execute.Sql("UPDATE umbracoUserGroup SET userGroupName = 'Translators' WHERE userGroupName = 'Translator'"); } diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index 83c31f6651..b15e11c21a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -104,6 +104,7 @@ namespace Umbraco.Core.Persistence.Repositories whereBuilder.Append(" OR "); } } + whereBuilder.Append(")"); } diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 12bdc12976..dbfb1a5bfe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -2,13 +2,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using Umbraco.Core; using Umbraco.Core.Logging; -using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; - using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index d6df61a7c5..08454c3b86 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -71,14 +71,6 @@ namespace Umbraco.Core.Services /// An enumerable list of IEnumerable GetPermissions(IUser user, params int[] nodeIds); - /// - /// Get permissions set for a user group and optional node ids - /// - /// Group to retrieve permissions for - /// Specifiying nothing will return all group permissions for all nodes - /// An enumerable list of - IEnumerable GetPermissions(IUserGroup group, params int[] nodeIds); - /// /// Gets the permissions for the provided user and path /// diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index b810840873..02cc3f874b 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -184,7 +184,6 @@ namespace Umbraco.Core.Services /// public IEnumerable
GetAllowedSections(int userId) { - var user = _userService.GetUserById(userId); if (user == null) { diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 11a476a10b..66aad86dbd 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -749,86 +749,41 @@ namespace Umbraco.Core.Services uow.Commit(); } - } + } /// - /// Get permissions set for a user and optional node ids + /// Get permissions set for a user and node Id /// - /// If no permissions are found for a particular entity then the user's default permissions will be applied /// User to retrieve permissions for - /// Specifiying nothing will return all user permissions for all nodes + /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of public IEnumerable GetPermissions(IUser user, params int[] nodeIds) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateUserRepository(uow)) + if (user.GroupsLoaded == false) { - // TODO: rework (to use groups - currently defaulting to user type) - - //if no permissions are assigned to a particular node then we will fill in those permissions with the user's defaults - var result = new List(); - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); - foreach (var id in missingIds) - { - result.Add( - new EntityPermission( - id, - user.DefaultPermissions.ToArray())); - } - - return result; + throw new InvalidOperationException("Cannot determine permissions for user as their associated groups are not loaded"); } - } - /// - /// Get permissions set for a group and optional node ids - /// - /// Group to retrieve permissions for - /// Specifiying nothing will return all permissions for all nodes - /// An enumerable list of - public IEnumerable GetPermissions(IUserGroup group, params int[] nodeIds) - { - throw new NotImplementedException(); - } - - /// - /// Get permissions set for a user and optional node ids - /// - /// User repository - /// User to retrieve permissions for - /// Specifiying nothing will return all user permissions for all nodes - /// An enumerable list of - private IEnumerable GetPermissions(IUserRepository repository, IUser user, params int[] nodeIds) - { var result = new List(); - - // TODO: rework to groups first, then user type - - // If no permissions are assigned to a particular node then, we will fill in those permissions with the user's defaults - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); - foreach (var id in missingIds) - { - result.Add( - new EntityPermission( - id, - user.DefaultPermissions.ToArray())); - } - - // Add or amend the existing permissions based on these foreach (var group in user.Groups) { - var groupPermissions = GetPermissions(group, nodeIds).ToList(); - foreach (var groupPermission in groupPermissions) + foreach (var permission in GetPermissions(group, nodeIds)) { - // Add group permission, ensuring we keep a unique value for the entity Id in the list - AddOrAmendPermission(result, groupPermission); + AddOrAmendPermissionList(result, permission); } } return result; } - private void AddOrAmendPermission(IList permissions, EntityPermission groupPermission) + /// + /// For an existing list of , takes a new and aggregates it. + /// If a permission for the entity associated with the new permission already exists, it's updated with those permissions to create a distinct, most permissive set. + /// If it doesn't, it's added to the list. + /// + /// List of already found permissions + /// New permission to aggregate + private void AddOrAmendPermissionList(IList permissions, EntityPermission groupPermission) { var existingPermission = permissions .SingleOrDefault(x => x.EntityId == groupPermission.EntityId); @@ -850,51 +805,38 @@ namespace Umbraco.Core.Services /// String indicating permissions for provided user and path public string GetPermissionsForPath(IUser user, string path) { - var nodeId = GetNodeIdFromPath(path); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateUserRepository(uow)) + if (user.GroupsLoaded == false) { - // Check for permissions directly assigned to the node for the user (if we have any, they take priority - // over anything defined on groups; we'll take the special "null" action as not being defined - // as this is set if someone sets permissions on a user and then removes them) - var permissions = GetPermissions(repository, user, nodeId) - .Where(IsNotNullActionPermission) - .ToList(); - var gotDirectlyAssignedUserPermissions = permissions.Any(); - - // If none found, and checking groups, get permissions from the groups - if (gotDirectlyAssignedUserPermissions == false) - { - foreach (var group in user.Groups) - { - var groupPermissions = GetPermissions(group, nodeId).ToList(); - permissions.AddRange(groupPermissions); - } - } - - // If none found directly on the user, get those defined on the user type - if (gotDirectlyAssignedUserPermissions == false) - { - var typePermissions = GetPermissions(repository, user, nodeId).ToList(); - permissions.AddRange(typePermissions); - } - - // Extract the net permissions from the path from the set of permissions found - string assignedPermissions; - if (TryGetAssignedPermissionsForNode(permissions, nodeId, out assignedPermissions)) - { - return assignedPermissions; - } - - // Exception to everything. If default cruds is empty and we're on root node; allow browse of root node - if (path == "-1") - { - return "F"; - } - - return string.Empty; + throw new InvalidOperationException("Cannot determine permissions for user as their associated groups are not loaded"); } + + var assignedPermissions = GetPermissionsForGroupsAndPath(user.Groups, path); + return GetAggregatePermissions(assignedPermissions); + } + + /// + /// Retrieves the permissions assigned to each group for a given path + /// + /// List of groups associated with the user + /// Path to check permissions for + /// List of strings indicating permissions for each groups + private IEnumerable GetPermissionsForGroupsAndPath(IEnumerable groups, string path) + { + return groups + .Select(g => GetPermissionsForPath(g, path)) + .ToList(); + } + + /// + /// Aggregates a set of permissions strings to return a unique permissions string containing the most permissive set + /// + /// List of permission strings + /// Single permission string + private static string GetAggregatePermissions(IEnumerable assignedPermissions) + { + return string.Join(string.Empty, assignedPermissions + .SelectMany(s => s.ToCharArray()) + .Distinct()); } /// @@ -906,14 +848,14 @@ namespace Umbraco.Core.Services public string GetPermissionsForPath(IUserGroup group, string path) { var nodeId = GetNodeIdFromPath(path); - - var permissions = GetPermissions(group).ToList(); - - string assignedPermissions; - TryGetAssignedPermissionsForNode(permissions, nodeId, out assignedPermissions); - return assignedPermissions; + return string.Join(string.Empty, GetPermissions(group, nodeId).Single().AssignedPermissions); } + /// + /// Parses a path to find the lowermost node id + /// + /// Path as string + /// Node id private static int GetNodeIdFromPath(string path) { return path.Contains(",") @@ -921,6 +863,29 @@ namespace Umbraco.Core.Services : int.Parse(path); } + /// + /// Get permissions set for a group and node Id + /// + /// Group to retrieve permissions for + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + private IEnumerable GetPermissions(IUserGroup group, params int[] nodeIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) + { + var explicitPermissions = repository.GetPermissionsForEntities(group.Id, nodeIds); + var result = new List(explicitPermissions); + + // If no permissions are assigned to a particular node then we will fill in those permissions with the group's defaults + var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); + result.AddRange(missingIds + .Select(i => new EntityPermission(i, group.Permissions.ToArray()))); + + return result; + } + } + private static bool IsNotNullActionPermission(EntityPermission x) { const string NullActionChar = "-"; diff --git a/src/Umbraco.Tests/Models/UserTests.cs b/src/Umbraco.Tests/Models/UserTests.cs index 8699a97698..5bdc842cc1 100644 --- a/src/Umbraco.Tests/Models/UserTests.cs +++ b/src/Umbraco.Tests/Models/UserTests.cs @@ -21,7 +21,6 @@ namespace Umbraco.Tests.Models CreateDate = DateTime.Now, Name = "Test", Comments = "comments", - DefaultPermissions = new[]{"a","b","c"}, DefaultToLiveEditing = false, Email = "test@test.com", Language = "en", @@ -48,9 +47,6 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.AllowedSections.Count(), item.AllowedSections.Count()); - Assert.AreNotSame(clone.DefaultPermissions, item.DefaultPermissions); - Assert.AreEqual(clone.DefaultPermissions.Count(), item.DefaultPermissions.Count()); - //Verify normal properties with reflection var allProps = clone.GetType().GetProperties(); foreach (var propertyInfo in allProps) @@ -72,7 +68,6 @@ namespace Umbraco.Tests.Models CreateDate = DateTime.Now, Name = "Test", Comments = "comments", - DefaultPermissions = new[] { "a", "b", "c" }, DefaultToLiveEditing = false, Email = "test@test.com", Language = "en", diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 5c7a87c240..d3ba021294 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -322,33 +322,10 @@ namespace Umbraco.Tests.Persistence.Repositories } } - [Test] - public void Default_User_Permissions_Based_On_User_Type() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - using (var repository = new UserRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax)) - { - - // Act - var user1 = MockedUser.CreateUser("1"); - repository.AddOrUpdate(user1); - unitOfWork.Commit(); - - // Assert - Assert.AreEqual(3, user1.DefaultPermissions.Count()); - Assert.AreEqual("A", user1.DefaultPermissions.ElementAt(0)); - Assert.AreEqual("B", user1.DefaultPermissions.ElementAt(1)); - Assert.AreEqual("C", user1.DefaultPermissions.ElementAt(2)); - } - } - private void AssertPropertyValues(IUser updatedItem, IUser originalUser) { Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); Assert.That(updatedItem.Name, Is.EqualTo(originalUser.Name)); - Assert.That(updatedItem.DefaultPermissions, Is.EqualTo(originalUser.DefaultPermissions)); Assert.That(updatedItem.Language, Is.EqualTo(originalUser.Language)); Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 4acdff7d2d..b8f23cee6c 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -506,7 +506,6 @@ namespace Umbraco.Tests.Services Assert.IsNotNull(updatedItem); Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); Assert.That(updatedItem.Name, Is.EqualTo(originalUser.Name)); - Assert.That(updatedItem.DefaultPermissions, Is.EqualTo(originalUser.DefaultPermissions)); Assert.That(updatedItem.Language, Is.EqualTo(originalUser.Language)); Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs index 9237574c51..53b53e634f 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs @@ -15,7 +15,6 @@ namespace Umbraco.Tests.TestHelpers.Entities Name = "TestUser" + suffix, RawPasswordValue = "testing", IsLockedOut = false, - DefaultPermissions = new[]{"A", "B", "C"}, StartContentId = -1, StartMediaId = -1, Email = "test" + suffix + "@test.com", diff --git a/src/umbraco.businesslogic/UserGroup.cs b/src/umbraco.businesslogic/UserGroup.cs index c04dad96e3..4a18a95c33 100644 --- a/src/umbraco.businesslogic/UserGroup.cs +++ b/src/umbraco.businesslogic/UserGroup.cs @@ -103,7 +103,7 @@ namespace umbraco.BusinessLogic } /// - /// Gets the id the user type + /// Gets the id the user group /// public int Id { @@ -111,7 +111,7 @@ namespace umbraco.BusinessLogic } /// - /// Gets the default permissions of the user type + /// Gets the default permissions of the user group /// public string DefaultPermissions { @@ -238,7 +238,7 @@ namespace umbraco.BusinessLogic public static UserGroup MakeNew(string name, string defaultPermissions, string alias) { //ensure that the current alias does not exist - //get the id for the new user type + //get the id for the new user group var existing = UserGroups.Find(ut => (ut.Alias == alias)); if (existing != null) @@ -261,7 +261,7 @@ namespace umbraco.BusinessLogic } /// - /// Gets the user type with the specied ID + /// Gets the user group with the specfied ID /// /// The id. /// From 93c400a005e36831c04b9047922ce5e1290ae124 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 29 Oct 2016 23:10:00 +0200 Subject: [PATCH 007/714] Fixed and added unit tests for permission checks --- .../Models/Membership/IUserGroup.cs | 2 + .../Models/Membership/UserGroup.cs | 5 ++ .../Repositories/ContentRepository.cs | 8 +-- .../Interfaces/IContentRepository.cs | 6 +-- src/Umbraco.Core/Services/ContentService.cs | 6 +-- src/Umbraco.Core/Services/IContentService.cs | 6 +-- .../Services/IMembershipUserService.cs | 1 + src/Umbraco.Core/Services/SectionService.cs | 2 +- src/Umbraco.Core/Services/UserService.cs | 9 ++-- .../Repositories/ContentRepositoryTest.cs | 24 +++++---- .../Repositories/UserGroupRepositoryTest.cs | 40 +++++++------- .../Repositories/UserRepositoryTest.cs | 42 +++++++++------ .../Services/SectionServiceTests.cs | 53 +++++++------------ .../Services/UserServiceTests.cs | 50 ++++++++++++----- .../TestHelpers/Entities/MockedUserGroup.cs | 27 +++++++--- 15 files changed, 167 insertions(+), 114 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index 8e9f862910..336caa4c47 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -28,5 +28,7 @@ namespace Umbraco.Core.Models.Membership void RemoveAllowedSection(string sectionAlias); void AddAllowedSection(string sectionAlias); + + void ClearAllowedSections(); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index ea5f891037..885a7cbe55 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -93,5 +93,10 @@ namespace Umbraco.Core.Models.Membership _sectionCollection.Add(sectionAlias); } } + + public void ClearAllowedSections() + { + _sectionCollection.Clear(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 1a544cc32e..0a244d30ef 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -780,15 +780,15 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; } /// - /// Assigns a single permission to the current content item for the specified user ids + /// Assigns a single permission to the current content item for the specified user group ids /// /// /// - /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds) + /// + public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) { var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.AssignEntityPermission(entity, permission, userIds); + repo.AssignEntityPermission(entity, permission, groupIds); } public IEnumerable GetPermissionsForEntity(int entityId) diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 5f74a21d7a..a0c379666b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -48,12 +48,12 @@ namespace Umbraco.Core.Persistence.Repositories IEnumerable GetByPublishedVersion(IQuery query); /// - /// Assigns a single permission to the current content item for the specified user ids + /// Assigns a single permission to the current content item for the specified user group ids /// /// /// - /// - void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds); + /// + void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); /// /// Gets the list of permissions for the content item diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 8aa7030a8f..b88678ac73 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -111,13 +111,13 @@ namespace Umbraco.Core.Services /// /// /// - /// - public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds) + /// + public void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { - repository.AssignEntityPermission(entity, permission, userIds); + repository.AssignEntityPermission(entity, permission, groupIds); } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 6ee384c413..d2f277dfbb 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -123,12 +123,12 @@ namespace Umbraco.Core.Services void ReplaceContentPermissions(EntityPermissionSet permissionSet); /// - /// Assigns a single permission to the current content item for the specified user ids + /// Assigns a single permission to the current content item for the specified user group ids /// /// /// - /// - void AssignContentPermission(IContent entity, char permission, IEnumerable userIds); + /// + void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds); /// /// Gets the list of permissions for the content item diff --git a/src/Umbraco.Core/Services/IMembershipUserService.cs b/src/Umbraco.Core/Services/IMembershipUserService.cs index 63dc4db37c..4143a871bf 100644 --- a/src/Umbraco.Core/Services/IMembershipUserService.cs +++ b/src/Umbraco.Core/Services/IMembershipUserService.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Services diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index 02cc3f874b..b12412b5f2 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -249,7 +249,7 @@ namespace Umbraco.Core.Services { //delete the assigned applications _uowProvider.GetUnitOfWork().Database.Execute( - "delete from umbracoUser2App where app = @appAlias", + "delete from umbracoUserGroup2App where app = @appAlias", new { appAlias = section.Alias }); //delete the assigned trees diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 66aad86dbd..45dc0c921d 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -878,9 +878,12 @@ namespace Umbraco.Core.Services var result = new List(explicitPermissions); // If no permissions are assigned to a particular node then we will fill in those permissions with the group's defaults - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); - result.AddRange(missingIds - .Select(i => new EntityPermission(i, group.Permissions.ToArray()))); + var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToList(); + if (missingIds.Any()) + { + result.AddRange(missingIds + .Select(i => new EntityPermission(i, group.Permissions.ToArray()))); + } return result; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index abcb5b3d6a..e9b86ee2d3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -1,27 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Xml.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence; - -using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; -using umbraco.editorControls.tinyMCE3; -using umbraco.interfaces; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Persistence.Repositories @@ -67,6 +59,11 @@ namespace Umbraco.Tests.Persistence.Repositories return repository; } + private UserGroupRepository CreateUserGroupRepository(IDatabaseUnitOfWork unitOfWork) + { + return new UserGroupRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); + } + [Test] public void Rebuild_Xml_Structures_With_Non_Latest_Version() { @@ -299,7 +296,14 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = new PetaPocoUnitOfWorkProvider(Logger); var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; + using (var repository = CreateUserGroupRepository(unitOfWork)) + { + var userGroup = MockedUserGroup.CreateUserGroup("1"); + repository.AddOrUpdate(userGroup); + unitOfWork.Commit(); + } + + ContentTypeRepository contentTypeRepository; using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) { var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); @@ -313,7 +317,7 @@ namespace Umbraco.Tests.Persistence.Repositories unitOfWork.Commit(); // Act - repository.AssignEntityPermission(parentPage, 'A', new int[] { 0 }); + repository.AssignEntityPermission(parentPage, 'A', new [] { 1 }); var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); repository.AddOrUpdate(childPage); unitOfWork.Commit(); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs index a29aeb8c29..6a2412bc63 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs @@ -325,18 +325,24 @@ namespace Umbraco.Tests.Persistence.Repositories // Act //add and remove a few times, this tests the internal collection + groups[0].ClearAllowedSections(); + groups[0].AddAllowedSection("content"); + groups[0].AddAllowedSection("media"); + groups[0].RemoveAllowedSection("content"); + groups[0].AddAllowedSection("content"); groups[0].AddAllowedSection("settings"); - groups[0].AddAllowedSection("settings"); - groups[0].RemoveAllowedSection("settings"); - groups[0].AddAllowedSection("settings"); - - groups[1].AddAllowedSection("developer"); //add the same even though it's already there - groups[2].AddAllowedSection("content"); + groups[0].AddAllowedSection("content"); + + groups[1].ClearAllowedSections(); + groups[1].AddAllowedSection("developer"); + + groups[2].ClearAllowedSections(); repository.AddOrUpdate(groups[0]); repository.AddOrUpdate(groups[1]); + repository.AddOrUpdate(groups[2]); unitOfWork.Commit(); // Assert @@ -345,13 +351,9 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.IsTrue(result[0].AllowedSections.Contains("content")); Assert.IsTrue(result[0].AllowedSections.Contains("media")); Assert.IsTrue(result[0].AllowedSections.Contains("settings")); - Assert.AreEqual(3, result[1].AllowedSections.Count()); - Assert.IsTrue(result[1].AllowedSections.Contains("content")); - Assert.IsTrue(result[1].AllowedSections.Contains("media")); + Assert.AreEqual(1, result[1].AllowedSections.Count()); Assert.IsTrue(result[1].AllowedSections.Contains("developer")); - Assert.AreEqual(2, result[2].AllowedSections.Count()); - Assert.IsTrue(result[1].AllowedSections.Contains("content")); - Assert.IsTrue(result[1].AllowedSections.Contains("media")); + Assert.AreEqual(0, result[2].AllowedSections.Count()); } } @@ -390,23 +392,23 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repository = CreateRepository(unitOfWork)) { - var user1 = MockedUserGroup.CreateUserGroup("1", new[] { "media" }); - var user2 = MockedUserGroup.CreateUserGroup("2", new[] { "settings" }); - var user3 = MockedUserGroup.CreateUserGroup("3", new[] { "settings" }); + var user1 = MockedUserGroup.CreateUserGroup("1", allowedSections: new[] { "test1" }); + var user2 = MockedUserGroup.CreateUserGroup("2", allowedSections: new[] { "test2" }); + var user3 = MockedUserGroup.CreateUserGroup("3", allowedSections: new[] { "test1" }); repository.AddOrUpdate(user1); repository.AddOrUpdate(user2); repository.AddOrUpdate(user3); unitOfWork.Commit(); // Act - - var groups = repository.GetGroupsAssignedToSection("test"); + var groups = repository.GetGroupsAssignedToSection("test1"); // Assert Assert.AreEqual(2, groups.Count()); var names = groups.Select(x => x.Name).ToArray(); - Assert.IsTrue(names.Contains("TestGroup1")); - Assert.IsTrue(names.Contains("TestGroup3")); + Assert.IsTrue(names.Contains("TestUserGroup1")); + Assert.IsFalse(names.Contains("TestUserGroup2")); + Assert.IsTrue(names.Contains("TestUserGroup3")); } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index d3ba021294..45c21e82c3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -30,10 +30,13 @@ namespace Umbraco.Tests.Persistence.Repositories private UserRepository CreateRepository(IDatabaseUnitOfWork unitOfWork) { - var repository = new UserRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); - return repository; + return new UserRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); } + private UserGroupRepository CreateUserGroupRepository(IDatabaseUnitOfWork unitOfWork) + { + return new UserGroupRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); + } [Test] public void Can_Perform_Add_On_UserRepository() @@ -43,7 +46,6 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repository = CreateRepository(unitOfWork)) { - var user = MockedUser.CreateUser(); // Act @@ -63,7 +65,6 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repository = CreateRepository(unitOfWork)) { - var user1 = MockedUser.CreateUser("1"); var use2 = MockedUser.CreateUser("2"); @@ -107,17 +108,14 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = new PetaPocoUnitOfWorkProvider(Logger); var unitOfWork = provider.GetUnitOfWork(); using (var repository = CreateRepository(unitOfWork)) + using (var userGroupRepository = CreateUserGroupRepository(unitOfWork)) { - var user = MockedUser.CreateUser(); - repository.AddOrUpdate(user); - unitOfWork.Commit(); + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository, unitOfWork); // Act var resolved = (User)repository.Get((int)user.Id); resolved.Name = "New Name"; - //the db column is not used, default permissions are taken from the user type's permissions, this is a getter only - //resolved.DefaultPermissions = "ZYX"; resolved.Language = "fr"; resolved.IsApproved = false; resolved.RawPasswordValue = "new"; @@ -134,7 +132,6 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert Assert.That(updatedItem.Id, Is.EqualTo(resolved.Id)); Assert.That(updatedItem.Name, Is.EqualTo(resolved.Name)); - //Assert.That(updatedItem.DefaultPermissions, Is.EqualTo(resolved.DefaultPermissions)); Assert.That(updatedItem.Language, Is.EqualTo(resolved.Language)); Assert.That(updatedItem.IsApproved, Is.EqualTo(resolved.IsApproved)); Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(resolved.RawPasswordValue)); @@ -143,7 +140,8 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(updatedItem.StartMediaId, Is.EqualTo(resolved.StartMediaId)); Assert.That(updatedItem.Email, Is.EqualTo(resolved.Email)); Assert.That(updatedItem.Username, Is.EqualTo(resolved.Username)); - Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(1)); + Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(2)); + Assert.IsTrue(updatedItem.AllowedSections.Contains("content")); Assert.IsTrue(updatedItem.AllowedSections.Contains("media")); } } @@ -213,13 +211,12 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = new PetaPocoUnitOfWorkProvider(Logger); var unitOfWork = provider.GetUnitOfWork(); using (var repository = CreateRepository(unitOfWork)) + using (var userGroupRepository = CreateUserGroupRepository(unitOfWork)) { - var user = MockedUser.CreateUser(); - repository.AddOrUpdate(user); - unitOfWork.Commit(); + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository, unitOfWork); // Act - var updatedItem = repository.Get((int) user.Id); + var updatedItem = repository.Get((int)user.Id); // Assert AssertPropertyValues(updatedItem, user); @@ -339,6 +336,21 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.IsTrue(updatedItem.AllowedSections.Contains("content")); } + private static User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository, IDatabaseUnitOfWork unitOfWork) + { + var group = MockedUserGroup.CreateUserGroup(); + userGroupRepository.AddOrUpdate(@group); + unitOfWork.Commit(); + + var user = MockedUser.CreateUser(); + repository.AddOrUpdate(user); + unitOfWork.Commit(); + + userGroupRepository.AddUsersToGroup(@group.Id, new[] { user.Id }); + unitOfWork.Commit(); + return user; + } + private IUser[] CreateAndCommitMultipleUsers(IUserRepository repository, IUnitOfWork unitOfWork) { var user1 = MockedUser.CreateUser("1"); diff --git a/src/Umbraco.Tests/Services/SectionServiceTests.cs b/src/Umbraco.Tests/Services/SectionServiceTests.cs index a300d76e60..5c51233892 100644 --- a/src/Umbraco.Tests/Services/SectionServiceTests.cs +++ b/src/Umbraco.Tests/Services/SectionServiceTests.cs @@ -29,34 +29,20 @@ namespace Umbraco.Tests.Services ServiceContext.SectionService.MakeNew("Settings", "settings", "icon-settings"); ServiceContext.SectionService.MakeNew("Developer", "developer", "icon-developer"); } - [Test] public void SectionService_Can_Get_Allowed_Sections_For_User() { // Arrange - var user = CreateUser(); + var user = CreateTestUser(); // Act var result = ServiceContext.SectionService.GetAllowedSections(user.Id).ToList(); // Assert - Assert.AreEqual(2, result.Count); + Assert.AreEqual(3, result.Count); } - [Test] - public void SectionService_Can_Get_Allowed_Sections_For_User_With_Groups() - { - // Arrange - var user = CreateUser(true); - - // Act - var result = ServiceContext.SectionService.GetAllowedSections(user.Id).ToList(); - - // Assert - Assert.AreEqual(4, result.Count); - } - - private IUser CreateUser(bool withGroups = false) + private IUser CreateTestUser() { var user = new User { @@ -66,26 +52,23 @@ namespace Umbraco.Tests.Services }; ServiceContext.UserService.Save(user, false); - if (withGroups) + var userGroupA = new UserGroup { - var userGroupA = new UserGroup - { - Alias = "GroupA", - Name = "Group A" - }; - userGroupA.AddAllowedSection("media"); - userGroupA.AddAllowedSection("settings"); - ServiceContext.UserService.SaveUserGroup(userGroupA, true, new[] { user.Id }, false); + Alias = "GroupA", + Name = "Group A" + }; + userGroupA.AddAllowedSection("media"); + userGroupA.AddAllowedSection("settings"); + ServiceContext.UserService.SaveUserGroup(userGroupA, true, new[] { user.Id }, false); - var userGroupB = new UserGroup - { - Alias = "GroupB", - Name = "Group B" - }; - userGroupB.AddAllowedSection("settings"); - userGroupB.AddAllowedSection("developer"); - ServiceContext.UserService.SaveUserGroup(userGroupB, true, new[] { user.Id }, false); - } + var userGroupB = new UserGroup + { + Alias = "GroupB", + Name = "Group B" + }; + userGroupB.AddAllowedSection("settings"); + userGroupB.AddAllowedSection("developer"); + ServiceContext.UserService.SaveUserGroup(userGroupB, true, new[] { user.Id }, false); return ServiceContext.UserService.GetUserById(user.Id); } diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index b8f23cee6c..795088f48f 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -39,7 +39,8 @@ namespace Umbraco.Tests.Services { // Arrange var userService = ServiceContext.UserService; - var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com"); + var user = CreateTestUser(); + var contentType = MockedContentTypes.CreateSimpleContentType(); ServiceContext.ContentTypeService.Save(contentType); var content = new[] @@ -65,7 +66,8 @@ namespace Umbraco.Tests.Services { // Arrange var userService = ServiceContext.UserService; - var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com"); + var user = CreateTestUser(); + var contentType = MockedContentTypes.CreateSimpleContentType(); ServiceContext.ContentTypeService.Save(contentType); var content = new[] @@ -75,14 +77,12 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType) }; ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { user.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { user.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { user.Id }); - - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { user.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { user.Id }); - - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { user.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { 1 }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { 1 }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { 1 }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { 1 }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { 1 }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { 1 }); // Act var permissions = userService.GetPermissions(user, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); @@ -346,6 +346,7 @@ namespace Umbraco.Tests.Services { Id = 1, Alias = "Group1", + Name = "Group 1" }; userGroup.AddAllowedSection("content"); userGroup.AddAllowedSection("mediat"); @@ -390,11 +391,13 @@ namespace Umbraco.Tests.Services { Id = 1, Alias = "Group1", + Name = "Group 2" }; var userGroup2 = new UserGroup { Id = 2, Alias = "Group2", + Name = "Group 2" }; ServiceContext.UserService.SaveUserGroup(userGroup1); ServiceContext.UserService.SaveUserGroup(userGroup2); @@ -422,16 +425,19 @@ namespace Umbraco.Tests.Services { Id = 1, Alias = "Group1", + Name = "Group 1" }; var userGroup2 = new UserGroup { Id = 2, Alias = "Group2", + Name = "Group 2" }; var userGroup3 = new UserGroup { - Id = 2, + Id = 3, Alias = "Group3", + Name = "Group 3" }; ServiceContext.UserService.SaveUserGroup(userGroup1); ServiceContext.UserService.SaveUserGroup(userGroup2); @@ -496,7 +502,7 @@ namespace Umbraco.Tests.Services public void Get_User_By_Username() { // Arrange - var originalUser = (User)ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com"); + var originalUser = CreateTestUser(); // Act @@ -516,5 +522,25 @@ namespace Umbraco.Tests.Services Assert.That(updatedItem.Username, Is.EqualTo(originalUser.Username)); Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(2)); } + + private IUser CreateTestUser() + { + var userGroup = new UserGroup + { + Id = 1, + Alias = "testGroup", + Name = "Test Group", + Permissions = "ABCDEFGHIJ1234567".ToCharArray().Select(x => x.ToString()) + }; + ServiceContext.UserService.SaveUserGroup(userGroup); + ServiceContext.UserService.AddSectionToAllUserGroups("content", 1); + ServiceContext.UserService.AddSectionToAllUserGroups("media", 1); + + var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com"); + user.AddGroup(userGroup); + user.SetGroupsLoaded(); + ServiceContext.UserService.Save(user); + return user; + } } } diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedUserGroup.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedUserGroup.cs index f266f55332..920c84f3c3 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedUserGroup.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedUserGroup.cs @@ -4,14 +4,29 @@ namespace Umbraco.Tests.TestHelpers.Entities { public class MockedUserGroup { - internal static UserGroup CreateUserGroup(string suffix = "", string[] permissions = null) + internal static UserGroup CreateUserGroup(string suffix = "", string[] permissions = null, string[] allowedSections = null) { - return new UserGroup() + var group = new UserGroup + { + Alias = "testUserGroup" + suffix, + Name = "TestUserGroup" + suffix, + Permissions = permissions ?? new[] { "A", "B", "C" } + }; + + if (allowedSections == null) + { + group.AddAllowedSection("content"); + group.AddAllowedSection("media"); + } + else + { + foreach (var allowedSection in allowedSections) { - Alias = "testUserGroup" + suffix, - Name = "TestUserGroup" + suffix, - Permissions = permissions ?? new[]{"A", "B", "C"} - }; + group.AddAllowedSection(allowedSection); + } + } + + return group; } } } \ No newline at end of file From 311401d1198d68f7c6fbf2608274356a68ed1391 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 29 Oct 2016 23:57:04 +0200 Subject: [PATCH 008/714] Ensured groups always loaded along with users --- .../Persistence/Repositories/UserRepository.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index dbfb1a5bfe..cad213d1a0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -68,8 +68,15 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoUser.id in (@ids)", new {ids = ids}); } - return ConvertFromDtos(Database.Fetch(sql)) + var users = ConvertFromDtos(Database.Fetch(sql)) .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. + + foreach (var user in users) + { + AssociateGroupsWithUser(user); + } + + return users; } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -84,10 +91,9 @@ namespace Umbraco.Core.Persistence.Repositories var users = ConvertFromDtos(dtos) .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. - // If a single user found (most likely from a look-up by an alternate key like email or username) then populate the groups - if (users.Length == 1) + foreach (var user in users) { - AssociateGroupsWithUser(users[0]); + AssociateGroupsWithUser(user); } return users; From f316b3f6942d6c2b38854d14e7e3b42424a24404 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 29 Oct 2016 23:57:31 +0200 Subject: [PATCH 009/714] Fixed display of node permissions when editing for a group --- .../umbraco/users/PermissionsEditor.js | 14 +++++++------- .../umbraco/users/PermissionsHandler.asmx.cs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/users/PermissionsEditor.js b/src/Umbraco.Web.UI/umbraco/users/PermissionsEditor.js index c193b24394..61a2c716ac 100644 --- a/src/Umbraco.Web.UI/umbraco/users/PermissionsEditor.js +++ b/src/Umbraco.Web.UI/umbraco/users/PermissionsEditor.js @@ -9,11 +9,11 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls"); $.fn.PermissionsEditor = function(opts) { return this.each(function() { var conf = $.extend({ - userId: -1, + groupId: -1, pPanelSelector: "", replacePChkBoxSelector: "" }, opts); - new Umbraco.Controls.PermissionsEditor().init($(this), conf.userId, $(conf.pPanelSelector), conf.replacePChkBoxSelector); + new Umbraco.Controls.PermissionsEditor().init($(this), conf.groupId, $(conf.pPanelSelector), conf.replacePChkBoxSelector); }); }; $.fn.PermissionsEditorAPI = function() { @@ -26,16 +26,16 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls"); /// return { //private members - _userID: -1, + _groupId: -1, _loadingContent: '







', _pPanel: null, _tree: null, _selectedNodes: new Array(), _chkReplaceSelector: null, - init: function(tree, uId, pPanel, chkReplaceSelector) { + init: function(tree, gId, pPanel, chkReplaceSelector) { ///constructor function - this._userID = parseInt(uId); + this._groupId = parseInt(gId); this._pPanel = pPanel; this._tree = tree; this._chkReplaceSelector = chkReplaceSelector; @@ -122,14 +122,14 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls"); }, _showNodePermissions: function(selectedIDs) { var _this = this; - umbraco.cms.presentation.user.PermissionsHandler.GetNodePermissions(this._userID, selectedIDs, function(r) { _this._showNodePermissionsCallback(r); }); + umbraco.cms.presentation.user.PermissionsHandler.GetNodePermissions(this._groupId, selectedIDs, function (r) { _this._showNodePermissionsCallback(r); }); }, _showNodePermissionsCallback: function(result) { this._pPanel.html(result); }, _savePermissions: function(nodeIDs, selectedPermissions, replaceChildren) { var _this = this; - umbraco.cms.presentation.user.PermissionsHandler.SaveNodePermissions(this._userID, nodeIDs, selectedPermissions, replaceChildren, function(r) { _this._savePermissionsHandler(r) }); + umbraco.cms.presentation.user.PermissionsHandler.SaveNodePermissions(this._groupId, nodeIDs, selectedPermissions, replaceChildren, function (r) { _this._savePermissionsHandler(r) }); }, _savePermissionsHandler: function(result) { if (UmbClientMgr.mainWindow().UmbSpeechBubble != null) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx.cs index b7c5e173cf..a570a5c7bc 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx.cs @@ -18,10 +18,10 @@ namespace umbraco.cms.presentation.user [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [ToolboxItem(false)] [ScriptService] - public class GroupPermissionsHandler + public class PermissionsHandler { /// - /// Loads the GroupNodePermissions UserControl with the appropriate properties, renders the contents and returns the output html. + /// Loads the NodePermissions UserControl with the appropriate properties, renders the contents and returns the output html. /// /// [WebMethod] From 6560f38cb09fcb1c0d60e25f8a7237ab804e00a7 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 30 Oct 2016 11:58:36 +0100 Subject: [PATCH 010/714] Fixed caching issues on editing node permissions --- src/Umbraco.Core/Cache/CacheKeys.cs | 2 - src/Umbraco.Core/Services/IUserService.cs | 18 +++- src/Umbraco.Core/Services/UserService.cs | 75 +++++++++------- .../Services/UserServiceTests.cs | 85 +++++++++++++++++-- src/Umbraco.Web/Cache/UserCacheRefresher.cs | 15 +--- .../Cache/UserGroupCacheRefresher.cs | 2 +- .../UserGroupPermissionsCacheRefresher.cs | 2 +- 7 files changed, 143 insertions(+), 56 deletions(-) diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 596bf2455b..02b01f1e78 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -56,8 +56,6 @@ namespace Umbraco.Core.Cache [EditorBrowsable(EditorBrowsableState.Never)] public const string UserCacheKey = "UmbracoUser"; - public const string UserPermissionsCacheKey = "UmbracoUserPermissions"; - public const string UserGroupPermissionsCacheKey = "UmbracoUserGroupPermissions"; [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 08454c3b86..dafe746fc8 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -71,6 +71,18 @@ namespace Umbraco.Core.Services /// An enumerable list of IEnumerable GetPermissions(IUser user, params int[] nodeIds); + /// + /// Get permissions set for a group and optional node Ids + /// + /// Group to retrieve permissions for + /// + /// Flag indicating if we want to get just the permissions directly assigned for the group and path, + /// or fall back to the group's default permissions when nothing is directly assigned + /// + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + IEnumerable GetPermissions(IUserGroup group, bool directlyAssignedOnly, params int[] nodeIds); + /// /// Gets the permissions for the provided user and path /// @@ -84,8 +96,12 @@ namespace Umbraco.Core.Services ///
/// User to check permissions for /// Path to check permissions for + /// + /// Flag indicating if we want to get just the permissions directly assigned for the group and path, + /// or fall back to the group's default permissions when nothing is directly assigned + /// /// String indicating permissions for provided user and path - string GetPermissionsForPath(IUserGroup group, string path); + string GetPermissionsForPath(IUserGroup group, string path, bool directlyAssignedOnly = true); /// /// Replaces the same permission set for a single group to any number of entities diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 45dc0c921d..81a71e0f05 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -767,7 +767,7 @@ namespace Umbraco.Core.Services var result = new List(); foreach (var group in user.Groups) { - foreach (var permission in GetPermissions(group, nodeIds)) + foreach (var permission in GetPermissions(group, false, nodeIds)) { AddOrAmendPermissionList(result, permission); } @@ -776,6 +776,39 @@ namespace Umbraco.Core.Services return result; } + /// + /// Get permissions set for a group and node Id + /// + /// Group to retrieve permissions for + /// + /// Flag indicating if we want to get just the permissions directly assigned for the group and path, + /// or fall back to the group's default permissions when nothing is directly assigned + /// + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + public IEnumerable GetPermissions(IUserGroup group, bool directlyAssignedOnly, params int[] nodeIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) + { + var explicitPermissions = repository.GetPermissionsForEntities(group.Id, nodeIds); + var result = new List(explicitPermissions); + + // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults + if (directlyAssignedOnly == false) + { + var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToList(); + if (missingIds.Any()) + { + result.AddRange(missingIds + .Select(i => new EntityPermission(i, group.Permissions.ToArray()))); + } + } + + return result; + } + } + /// /// For an existing list of , takes a new and aggregates it. /// If a permission for the entity associated with the new permission already exists, it's updated with those permissions to create a distinct, most permissive set. @@ -823,7 +856,7 @@ namespace Umbraco.Core.Services private IEnumerable GetPermissionsForGroupsAndPath(IEnumerable groups, string path) { return groups - .Select(g => GetPermissionsForPath(g, path)) + .Select(g => GetPermissionsForPath(g, path, directlyAssignedOnly: false)) .ToList(); } @@ -844,11 +877,19 @@ namespace Umbraco.Core.Services /// /// User to check permissions for /// Path to check permissions for + /// + /// Flag indicating if we want to get just the permissions directly assigned for the group and path, + /// or fall back to the group's default permissions when nothing is directly assigned + /// /// String indicating permissions for provided user and path - public string GetPermissionsForPath(IUserGroup group, string path) + public string GetPermissionsForPath(IUserGroup group, string path, bool directlyAssignedOnly = true) { var nodeId = GetNodeIdFromPath(path); - return string.Join(string.Empty, GetPermissions(group, nodeId).Single().AssignedPermissions); + var permission = GetPermissions(group, directlyAssignedOnly, nodeId) + .SingleOrDefault(); + return permission != null + ? string.Join(string.Empty, permission.AssignedPermissions) + : string.Empty; } /// @@ -863,32 +904,6 @@ namespace Umbraco.Core.Services : int.Parse(path); } - /// - /// Get permissions set for a group and node Id - /// - /// Group to retrieve permissions for - /// Specifiying nothing will return all permissions for all nodes - /// An enumerable list of - private IEnumerable GetPermissions(IUserGroup group, params int[] nodeIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateUserGroupRepository(uow)) - { - var explicitPermissions = repository.GetPermissionsForEntities(group.Id, nodeIds); - var result = new List(explicitPermissions); - - // If no permissions are assigned to a particular node then we will fill in those permissions with the group's defaults - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToList(); - if (missingIds.Any()) - { - result.AddRange(missingIds - .Select(i => new EntityPermission(i, group.Permissions.ToArray()))); - } - - return result; - } - } - private static bool IsNotNullActionPermission(EntityPermission x) { const string NullActionChar = "-"; diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 795088f48f..8992eebd95 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -93,7 +93,72 @@ namespace Umbraco.Tests.Services Assert.AreEqual(2, permissions.ElementAt(1).AssignedPermissions.Count()); Assert.AreEqual(1, permissions.ElementAt(2).AssignedPermissions.Count()); } - + + [Test] + public void UserService_Get_UserGroup_Assigned_Permissions() + { + // Arrange + var userService = ServiceContext.UserService; + var userGroup = CreateTestUserGroup(); + + var contentType = MockedContentTypes.CreateSimpleContentType(); + ServiceContext.ContentTypeService.Save(contentType); + var content = new[] + { + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType) + }; + ServiceContext.ContentService.Save(content); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + + // Act + var permissions = userService.GetPermissions(userGroup, true, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); + + //assert + Assert.AreEqual(3, permissions.Count()); + Assert.AreEqual(3, permissions.ElementAt(0).AssignedPermissions.Count()); + Assert.AreEqual(2, permissions.ElementAt(1).AssignedPermissions.Count()); + Assert.AreEqual(1, permissions.ElementAt(2).AssignedPermissions.Count()); + } + + [Test] + public void UserService_Get_UserGroup_Assigned_And_Default_Permissions() + { + // Arrange + var userService = ServiceContext.UserService; + var userGroup = CreateTestUserGroup(); + + var contentType = MockedContentTypes.CreateSimpleContentType(); + ServiceContext.ContentTypeService.Save(contentType); + var content = new[] + { + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType) + }; + ServiceContext.ContentService.Save(content); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + + // Act + var permissions = userService.GetPermissions(userGroup, false, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); + + //assert + Assert.AreEqual(3, permissions.Count()); + Assert.AreEqual(3, permissions.ElementAt(0).AssignedPermissions.Count()); + Assert.AreEqual(2, permissions.ElementAt(1).AssignedPermissions.Count()); + Assert.AreEqual(17, permissions.ElementAt(2).AssignedPermissions.Count()); + } + [Test] public void Can_Delete_User() { @@ -524,6 +589,17 @@ namespace Umbraco.Tests.Services } private IUser CreateTestUser() + { + var userGroup = CreateTestUserGroup(); + + var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com"); + user.AddGroup(userGroup); + user.SetGroupsLoaded(); + ServiceContext.UserService.Save(user); + return user; + } + + private UserGroup CreateTestUserGroup() { var userGroup = new UserGroup { @@ -535,12 +611,7 @@ namespace Umbraco.Tests.Services ServiceContext.UserService.SaveUserGroup(userGroup); ServiceContext.UserService.AddSectionToAllUserGroups("content", 1); ServiceContext.UserService.AddSectionToAllUserGroups("media", 1); - - var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com"); - user.AddGroup(userGroup); - user.SetGroupsLoaded(); - ServiceContext.UserService.Save(user); - return user; + return userGroup; } } } diff --git a/src/Umbraco.Web/Cache/UserCacheRefresher.cs b/src/Umbraco.Web/Cache/UserCacheRefresher.cs index 6f9cc7db80..861d5e6abf 100644 --- a/src/Umbraco.Web/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserCacheRefresher.cs @@ -2,9 +2,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models.Membership; - using Umbraco.Core.Persistence.Repositories; -using umbraco.interfaces; namespace Umbraco.Web.Cache { @@ -31,8 +29,6 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { ClearAllIsolatedCacheByEntityType(); - if (UserPermissionsCache) - UserPermissionsCache.Result.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); base.RefreshAll(); } @@ -47,17 +43,8 @@ namespace Umbraco.Web.Cache var userCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); if (userCache) userCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - - if (UserPermissionsCache) - UserPermissionsCache.Result.ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); - + base.Remove(id); } - - private Attempt UserPermissionsCache - { - get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } - } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs index 60b5c9a710..5f36f60bab 100644 --- a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs @@ -60,7 +60,7 @@ namespace Umbraco.Web.Cache private Attempt UserGroupPermissionsCache { - get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } + get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs b/src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs index cf091b6618..8ea1b26f47 100644 --- a/src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs @@ -52,7 +52,7 @@ namespace Umbraco.Web.Cache private Attempt UserGroupPermissionsCache { - get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } + get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } } } } \ No newline at end of file From 8bb3685e5deb3db23736fb82d8a2f4328c7f6532 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 30 Oct 2016 13:53:43 +0100 Subject: [PATCH 011/714] Fixed test failure reported on CI --- src/Umbraco.Core/Services/SectionService.cs | 4 +++- src/Umbraco.Tests/Services/SectionServiceTests.cs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index b12412b5f2..02c0b8c14a 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -211,7 +211,9 @@ namespace Umbraco.Core.Services /// The application icon, which has to be located in umbraco/images/tray folder. public void MakeNew(string name, string alias, string icon) { - MakeNew(name, alias, icon, GetSections().Max(x => x.SortOrder) + 1); + var sections = GetSections(); + var nextSortOrder = sections != null ? GetSections().Max(x => x.SortOrder) + 1 : 1; + MakeNew(name, alias, icon, nextSortOrder); } /// diff --git a/src/Umbraco.Tests/Services/SectionServiceTests.cs b/src/Umbraco.Tests/Services/SectionServiceTests.cs index 5c51233892..556203bb61 100644 --- a/src/Umbraco.Tests/Services/SectionServiceTests.cs +++ b/src/Umbraco.Tests/Services/SectionServiceTests.cs @@ -29,6 +29,7 @@ namespace Umbraco.Tests.Services ServiceContext.SectionService.MakeNew("Settings", "settings", "icon-settings"); ServiceContext.SectionService.MakeNew("Developer", "developer", "icon-developer"); } + [Test] public void SectionService_Can_Get_Allowed_Sections_For_User() { From ece61a45d8fb35546a0be4be03fb2c287eecc378 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 30 Oct 2016 14:16:01 +0100 Subject: [PATCH 012/714] Fixed test failure reported on CI (2) --- src/Umbraco.Core/Services/SectionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index 02c0b8c14a..87447c06f0 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -212,7 +212,7 @@ namespace Umbraco.Core.Services public void MakeNew(string name, string alias, string icon) { var sections = GetSections(); - var nextSortOrder = sections != null ? GetSections().Max(x => x.SortOrder) + 1 : 1; + var nextSortOrder = sections != null ? sections.Max(x => x.SortOrder) + 1 : 1; MakeNew(name, alias, icon, nextSortOrder); } From d7aba9ce4a80c87c67d6cbfd79b8c1f7a330a624 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 30 Oct 2016 16:10:00 +0100 Subject: [PATCH 013/714] Fixed test failure reported on CI (3) --- src/Umbraco.Core/Services/SectionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index 87447c06f0..9de793e37d 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -212,7 +212,7 @@ namespace Umbraco.Core.Services public void MakeNew(string name, string alias, string icon) { var sections = GetSections(); - var nextSortOrder = sections != null ? sections.Max(x => x.SortOrder) + 1 : 1; + var nextSortOrder = sections != null && sections.Any() ? sections.Max(x => x.SortOrder) + 1 : 1; MakeNew(name, alias, icon, nextSortOrder); } From 2363cdd17b6de24b298e7082ad0c59a0c87e5dd8 Mon Sep 17 00:00:00 2001 From: Shane Marsden Date: Thu, 3 Nov 2016 17:00:36 +0000 Subject: [PATCH 014/714] Painstakingly went through every image file one by one, looking for references, checking if file looked v6ish, checking for constructed filenames etc --- .../Umbraco/Images/pinnedIcons/task_content.ico | Bin 1150 -> 0 bytes .../Umbraco/Images/pinnedIcons/task_default.ico | Bin 1150 -> 0 bytes .../Images/pinnedIcons/task_developer.ico | Bin 1150 -> 0 bytes .../Umbraco/Images/pinnedIcons/task_media.ico | Bin 1150 -> 0 bytes .../Umbraco/Images/pinnedIcons/task_member.ico | Bin 1150 -> 0 bytes .../Images/pinnedIcons/task_settings.ico | Bin 1150 -> 0 bytes .../Umbraco/Images/pinnedIcons/task_users.ico | Bin 1150 -> 0 bytes .../Umbraco/Images/pinnedIcons/umb.ico | Bin 17542 -> 0 bytes src/Umbraco.Web.UI/umbraco/css/background.gif | Bin 151 -> 0 bytes src/Umbraco.Web.UI/umbraco/css/splitter.gif | Bin 88 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/Lminus.png | Bin 219 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/Lplus.png | Bin 224 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/Tminus.png | Bin 207 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/Tplus.png | Bin 222 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/aboutNew.png | Bin 1178 -> 0 bytes .../umbraco/images/actions/sprites.png | Bin 12301 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/arrawBack.gif | Bin 834 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/arrowDown.gif | Bin 832 -> 0 bytes .../umbraco/images/arrowForward.gif | Bin 834 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/audit.png | Bin 897 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/back.png | Bin 422 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/blank.png | Bin 144 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/c_b.gif | Bin 92 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/c_b_label.gif | Bin 96 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/c_bl.gif | Bin 211 -> 0 bytes .../umbraco/images/c_bl_label.gif | Bin 948 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/c_br.gif | Bin 240 -> 0 bytes .../umbraco/images/c_br_label.gif | Bin 1031 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/c_r.gif | Bin 93 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/c_t.gif | Bin 96 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/c_tl.gif | Bin 242 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/c_tr.gif | Bin 434 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/collapse.png | Bin 388 -> 0 bytes .../umbraco/images/copy.small.png | Bin 489 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/cut.small.png | Bin 472 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/date.gif | Bin 253 -> 0 bytes .../umbraco/images/delete_button.png | Bin 582 -> 0 bytes .../images/developer/customControlIcon.png | Bin 842 -> 0 bytes .../umbraco/images/developer/pythonIcon.png | Bin 30235 -> 0 bytes .../images/developer/usercontrolIcon.png | Bin 775 -> 0 bytes .../umbraco/images/developer/xsltIcon.png | Bin 776 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/dialogBg.png | Bin 255 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/domain.gif | Bin 362 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/domain_on.png | Bin 472 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/expand.png | Bin 400 -> 0 bytes .../umbraco/images/exportDocumenttype.png | Bin 617 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/false.png | Bin 894 -> 0 bytes .../umbraco/images/find.small.png | Bin 390 -> 0 bytes .../umbraco/images/findDocument.gif | Bin 967 -> 0 bytes .../umbraco/images/findDocument.png | Bin 617 -> 0 bytes .../umbraco/images/folder.small.png | Bin 413 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/forward.png | Bin 418 -> 0 bytes .../umbraco/images/gradientBackground.png | Bin 1321 -> 0 bytes .../umbraco/images/gradientLine.gif | Bin 1482 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/help.gif | Bin 1013 -> 0 bytes .../umbraco/images/htmldoc.small.png | Bin 507 -> 0 bytes .../umbraco/images/importDocumenttype.png | Bin 601 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/loginBg.png | Bin 17566 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/logout.png | Bin 1011 -> 0 bytes .../umbraco/images/logout_small.gif | Bin 996 -> 0 bytes .../umbraco/images/logout_small.png | Bin 592 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/macro.gif | Bin 115 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/new.gif | Bin 246 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/new.png | Bin 1035 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/newStar.gif | Bin 989 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/notepad.png | Bin 489 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/notify.gif | Bin 1003 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/notifyOld.gif | Bin 1003 -> 0 bytes .../umbraco/images/openfoldericon.png | Bin 232 -> 0 bytes .../umbraco/images/options.small.png | Bin 396 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/package2.png | Bin 966 -> 0 bytes .../umbraco/images/paste.small.png | Bin 632 -> 0 bytes .../umbraco/images/permission.gif | Bin 1009 -> 0 bytes .../umbraco/images/permission.png | Bin 581 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/protect.gif | Bin 1023 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/protect.png | Bin 628 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/publish.png | Bin 717 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/refresh.png | Bin 502 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/rollback.gif | Bin 588 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/rollback.png | Bin 626 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/save.png | Bin 549 -> 0 bytes .../umbraco/images/sendToTranslate.png | Bin 1047 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/sort.gif | Bin 388 -> 0 bytes .../umbraco/images/sort.small.png | Bin 400 -> 0 bytes .../umbraco/images/speechBubble/error.gif | Bin 582 -> 0 bytes .../umbraco/images/speechBubble/error.png | Bin 1915 -> 0 bytes .../umbraco/images/speechBubble/info.png | Bin 1542 -> 0 bytes .../umbraco/images/speechBubble/save.gif | Bin 562 -> 0 bytes .../umbraco/images/speechBubble/save.png | Bin 1036 -> 0 bytes .../images/speechBubble/speechbubble.png | Bin 12444 -> 0 bytes .../images/speechBubble/speechbubbleShadow.png | Bin 916 -> 0 bytes .../speechBubble/speechbubbleShadowNew.gif | Bin 1774 -> 0 bytes .../images/speechBubble/speechbubble_shadow.gif | Bin 660 -> 0 bytes .../umbraco/images/speechBubble/success.png | Bin 1036 -> 0 bytes .../umbraco/images/speechBubble/warning.png | Bin 1188 -> 0 bytes .../umbraco/images/thumbnails/developer.png | Bin 19431 -> 0 bytes .../umbraco/images/thumbnails/member.png | Bin 15119 -> 0 bytes .../umbraco/images/thumbnails/memberGroup.png | Bin 19497 -> 0 bytes .../umbraco/images/thumbnails/template.png | Bin 11883 -> 0 bytes .../umbraco/images/thumbnails/xml.png | Bin 12153 -> 0 bytes src/Umbraco.Web.UI/umbraco/images/true.png | Bin 612 -> 0 bytes .../umbraco/images/umbraco/bin_closed.png | Bin 363 -> 0 bytes .../images/umbraco/developerCacheItem.gif | Bin 577 -> 0 bytes .../images/umbraco/developerCacheTypes.gif | Bin 578 -> 0 bytes .../images/umbraco/developerDatatype.gif | Bin 125 -> 0 bytes .../umbraco/images/umbraco/developerMacro.gif | Bin 604 -> 0 bytes .../images/umbraco/developerRegistry.gif | Bin 118 -> 0 bytes .../images/umbraco/developerRegistryItem.gif | Bin 123 -> 0 bytes .../umbraco/images/umbraco/developerXslt.gif | Bin 410 -> 0 bytes .../umbraco/images/umbraco/doc2.gif | Bin 604 -> 0 bytes .../umbraco/images/umbraco/doc3.gif | Bin 616 -> 0 bytes .../umbraco/images/umbraco/doc4.gif | Bin 597 -> 0 bytes .../umbraco/images/umbraco/doc5.gif | Bin 609 -> 0 bytes .../umbraco/images/umbraco/docPic.gif | Bin 607 -> 0 bytes .../umbraco/images/umbraco/mediaMovie.gif | Bin 619 -> 0 bytes .../umbraco/images/umbraco/memberGroup.gif | Bin 1000 -> 0 bytes .../umbraco/images/umbraco/memberType.gif | Bin 1012 -> 0 bytes .../umbraco/images/umbraco/nitros.gif | Bin 525 -> 0 bytes .../umbraco/images/umbraco/package.gif | Bin 1021 -> 0 bytes .../umbraco/images/umbraco/repository.gif | Bin 1062 -> 0 bytes .../umbraco/images/umbraco/settingAgent.gif | Bin 145 -> 0 bytes .../umbraco/images/umbraco/settingCssItem.gif | Bin 299 -> 0 bytes .../images/umbraco/settingDataTypeChild.gif | Bin 144 -> 0 bytes .../umbraco/images/umbraco/settingLanguage.gif | Bin 387 -> 0 bytes .../images/umbraco/settingMasterDatatype.gif | Bin 587 -> 0 bytes .../images/umbraco/settingMasterTemplate.gif | Bin 235 -> 0 bytes .../umbraco/images/umbraco/settingSkin.gif | Bin 1655 -> 0 bytes .../umbraco/images/umbraco/settingTemplate.gif | Bin 252 -> 0 bytes .../umbraco/images/umbraco/settingView.gif | Bin 1644 -> 0 bytes .../umbraco/images/umbraco/settingXML.gif | Bin 268 -> 0 bytes .../umbraco/images/umbraco/settingsScript.gif | Bin 553 -> 0 bytes .../umbraco/images/umbraco/statistik.gif | Bin 305 -> 0 bytes .../umbraco/images/umbraco/uploadpackage.gif | Bin 1035 -> 0 bytes .../umbraco/images/umbraco/user.gif | Bin 998 -> 0 bytes .../umbraco/images/umbraco/userGroup.gif | Bin 1000 -> 0 bytes .../umbraco/images/umbraco/userType.gif | Bin 1012 -> 0 bytes .../Installer/images/bg-bhuiness-cl.gif | Bin 19424 -> 0 bytes .../Installer/images/bg-bhuiness-cr.gif | Bin 46684 -> 0 bytes .../Installer/images/bg-blog-cl.gif | Bin 18564 -> 0 bytes .../Installer/images/bg-blog-cr.gif | Bin 50756 -> 0 bytes .../Installer/images/bg-img-ie.png | Bin 3915 -> 0 bytes .../umbraco_client/Installer/images/bg-img.png | Bin 2452 -> 0 bytes .../Installer/images/bg-normal-cl.gif | Bin 18097 -> 0 bytes .../Installer/images/bg-normal-cr.gif | Bin 46703 -> 0 bytes .../Installer/images/bg-personal-cl.gif | Bin 18097 -> 0 bytes .../Installer/images/bg-personal-cr.gif | Bin 46703 -> 0 bytes .../Installer/images/bg-simple-cl.gif | Bin 17705 -> 0 bytes .../Installer/images/bg-simple-cr.gif | Bin 42137 -> 0 bytes .../Installer/images/btn-blog.png | Bin 22650 -> 0 bytes .../Installer/images/btn-buisness-repeat.png | Bin 16942 -> 0 bytes .../Installer/images/btn-buisness.png | Bin 26481 -> 0 bytes .../Installer/images/btn-confirm.png | Bin 3113 -> 0 bytes .../Installer/images/btn-create-hover.png | Bin 2243 -> 0 bytes .../Installer/images/btn-no-thanks.png | Bin 25962 -> 0 bytes .../Installer/images/btn-personal.png | Bin 30788 -> 0 bytes .../Installer/images/btn-simple.png | Bin 28477 -> 0 bytes .../umbraco_client/Installer/images/img01.jpg | Bin 3189 -> 0 bytes .../umbraco_client/Installer/images/img02.jpg | Bin 3533 -> 0 bytes .../umbraco_client/Installer/images/img03.jpg | Bin 6205 -> 0 bytes .../umbraco_client/Installer/images/img04.jpg | Bin 5029 -> 0 bytes .../umbraco_client/Installer/images/img05.jpg | Bin 6113 -> 0 bytes .../umbraco_client/Installer/images/img06.jpg | Bin 4408 -> 0 bytes .../umbraco_client/Installer/images/img07.jpg | Bin 5924 -> 0 bytes .../umbraco_client/Installer/images/img08.jpg | Bin 4535 -> 0 bytes .../umbraco_client/Installer/images/img09.jpg | Bin 16018 -> 0 bytes .../umbraco_client/Installer/images/img10.jpg | Bin 4173 -> 0 bytes .../umbraco_client/Installer/images/img11.jpg | Bin 6618 -> 0 bytes .../umbraco_client/Installer/images/loader.gif | Bin 2113 -> 0 bytes .../umbraco_client/Installer/images/pbar.gif | Bin 1026 -> 0 bytes .../umbraco_client/Tree/Themes/marker.gif | Bin 61 -> 0 bytes .../umbraco_client/Tree/Themes/marker_rtl.gif | Bin 62 -> 0 bytes .../umbraco_client/Tree/Themes/plus.gif | Bin 52 -> 0 bytes .../umbraco_client/Tree/Themes/remove.png | Bin 720 -> 0 bytes .../umbraco_client/Tree/Themes/rename.png | Bin 3867 -> 0 bytes .../Tree/Themes/umbraco/context.gif | Bin 35 -> 0 bytes .../Tree/Themes/umbraco/create.png | Bin 3329 -> 0 bytes .../umbraco_client/Tree/Themes/umbraco/dot.gif | Bin 43 -> 0 bytes .../umbraco_client/Tree/Themes/umbraco/f.png | Bin 3237 -> 0 bytes .../Tree/Themes/umbraco/fminus.gif | Bin 60 -> 0 bytes .../Tree/Themes/umbraco/fminus_rtl.gif | Bin 59 -> 0 bytes .../Tree/Themes/umbraco/fplus.gif | Bin 108 -> 0 bytes .../Tree/Themes/umbraco/fplus_rtl.gif | Bin 108 -> 0 bytes .../Tree/Themes/umbraco/lastli.gif | Bin 55 -> 0 bytes .../Tree/Themes/umbraco/lastli_rtl.gif | Bin 54 -> 0 bytes .../umbraco_client/Tree/Themes/umbraco/li.gif | Bin 47 -> 0 bytes .../umbraco_client/Tree/sprites_ie6.gif | Bin 7307 -> 0 bytes .../colorpicker/images/custom_background.png | Bin 1916 -> 0 bytes .../colorpicker/images/custom_hex.png | Bin 562 -> 0 bytes .../colorpicker/images/custom_hsb_b.png | Bin 1097 -> 0 bytes .../colorpicker/images/custom_hsb_h.png | Bin 970 -> 0 bytes .../colorpicker/images/custom_hsb_s.png | Bin 1168 -> 0 bytes .../colorpicker/images/custom_indic.gif | Bin 86 -> 0 bytes .../colorpicker/images/custom_rgb_b.png | Bin 1008 -> 0 bytes .../colorpicker/images/custom_rgb_g.png | Bin 1069 -> 0 bytes .../colorpicker/images/custom_rgb_r.png | Bin 1018 -> 0 bytes .../colorpicker/images/custom_submit.png | Bin 997 -> 0 bytes .../colorpicker/images/select.png | Bin 506 -> 0 bytes .../colorpicker/images/select2.png | Bin 518 -> 0 bytes .../colorpicker/images/slider.png | Bin 315 -> 0 bytes .../datepicker/aqua/active-bg.gif | Bin 89 -> 0 bytes .../umbraco_client/datepicker/aqua/dark-bg.gif | Bin 85 -> 0 bytes .../umbraco_client/datepicker/aqua/hover-bg.gif | Bin 89 -> 0 bytes .../datepicker/aqua/menuarrow.gif | Bin 49 -> 0 bytes .../datepicker/aqua/normal-bg.gif | Bin 110 -> 0 bytes .../datepicker/aqua/rowhover-bg.gif | Bin 110 -> 0 bytes .../datepicker/aqua/status-bg.gif | Bin 116 -> 0 bytes .../umbraco_client/datepicker/aqua/theme.css | 2 +- .../umbraco_client/datepicker/aqua/title-bg.gif | Bin 116 -> 0 bytes .../umbraco_client/datepicker/aqua/today-bg.gif | Bin 1122 -> 0 bytes .../datepicker/images/calPickerIcon.png | Bin 674 -> 0 bytes .../datepicker/images/calPickerIconHover.png | Bin 633 -> 0 bytes .../propertypane/images/proppane_bg.png | Bin 554 -> 0 bytes .../scrollingmenu/images/arrawBack.gif | Bin 834 -> 0 bytes .../scrollingmenu/images/arrowForward.gif | Bin 834 -> 0 bytes .../scrollingmenu/images/background.gif | Bin 183 -> 0 bytes .../scrollingmenu/images/button/buttonbg.gif | Bin 273 -> 0 bytes .../images/button/buttonbgdown.gif | Bin 273 -> 0 bytes 217 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_content.ico delete mode 100644 src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_default.ico delete mode 100644 src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_developer.ico delete mode 100644 src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_media.ico delete mode 100644 src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_member.ico delete mode 100644 src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_settings.ico delete mode 100644 src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_users.ico delete mode 100644 src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/umb.ico delete mode 100644 src/Umbraco.Web.UI/umbraco/css/background.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/css/splitter.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/Lminus.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/Lplus.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/Tminus.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/Tplus.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/aboutNew.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/actions/sprites.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/arrawBack.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/arrowDown.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/arrowForward.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/audit.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/back.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/blank.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_b.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_b_label.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_bl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_bl_label.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_br.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_br_label.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_r.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_t.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_tl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/c_tr.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/collapse.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/copy.small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/cut.small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/date.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/delete_button.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/developer/customControlIcon.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/developer/pythonIcon.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/developer/usercontrolIcon.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/developer/xsltIcon.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/dialogBg.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/domain.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/domain_on.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/expand.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/exportDocumenttype.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/false.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/find.small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/findDocument.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/findDocument.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/folder.small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/forward.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/gradientBackground.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/gradientLine.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/help.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/htmldoc.small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/importDocumenttype.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/loginBg.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/logout.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/logout_small.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/logout_small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/macro.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/new.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/new.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/newStar.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/notepad.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/notify.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/notifyOld.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/openfoldericon.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/options.small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/package2.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/paste.small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/permission.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/permission.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/protect.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/protect.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/publish.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/refresh.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/rollback.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/rollback.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/save.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/sendToTranslate.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/sort.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/sort.small.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/error.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/error.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/info.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/save.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/save.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubble.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubbleShadow.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubbleShadowNew.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubble_shadow.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/success.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/speechBubble/warning.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/thumbnails/developer.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/thumbnails/member.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/thumbnails/memberGroup.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/thumbnails/template.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/thumbnails/xml.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/true.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/bin_closed.png delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/developerCacheItem.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/developerCacheTypes.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/developerDatatype.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/developerMacro.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/developerRegistry.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/developerRegistryItem.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/developerXslt.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/doc2.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/doc3.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/doc4.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/doc5.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/docPic.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/mediaMovie.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/memberGroup.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/memberType.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/nitros.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/package.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/repository.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingAgent.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingCssItem.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingDataTypeChild.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingLanguage.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingMasterDatatype.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingMasterTemplate.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingSkin.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingTemplate.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingView.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingXML.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/settingsScript.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/statistik.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/uploadpackage.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/user.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/userGroup.gif delete mode 100644 src/Umbraco.Web.UI/umbraco/images/umbraco/userType.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-bhuiness-cl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-bhuiness-cr.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-blog-cl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-blog-cr.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-img-ie.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-img.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-normal-cl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-normal-cr.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-personal-cl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-personal-cr.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-simple-cl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-simple-cr.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-blog.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-buisness-repeat.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-buisness.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-confirm.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-create-hover.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-no-thanks.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-personal.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-simple.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img01.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img02.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img03.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img04.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img05.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img06.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img07.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img08.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img09.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img10.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/img11.jpg delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/loader.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Installer/images/pbar.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/marker.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/marker_rtl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/plus.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/remove.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/rename.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/context.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/create.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/dot.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/f.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fminus.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fminus_rtl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fplus.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fplus_rtl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/lastli.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/lastli_rtl.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/li.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/Tree/sprites_ie6.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_background.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_hex.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_hsb_b.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_hsb_h.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_hsb_s.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_indic.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_rgb_b.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_rgb_g.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_rgb_r.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_submit.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/select.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/select2.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/colorpicker/images/slider.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/active-bg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/dark-bg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/hover-bg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/menuarrow.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/normal-bg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/rowhover-bg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/status-bg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/title-bg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/today-bg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/images/calPickerIcon.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/datepicker/images/calPickerIconHover.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/propertypane/images/proppane_bg.png delete mode 100644 src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/arrawBack.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/arrowForward.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/background.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/button/buttonbg.gif delete mode 100644 src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/button/buttonbgdown.gif diff --git a/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_content.ico b/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_content.ico deleted file mode 100644 index 06aed6d45d10355cfeb8a975db7a6f6b49df0c39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmbW0OH30{6o!XrWXsC6ON}B#V@x1wh-eTEQPeeH%R`OE4bcRQA|?=YgJ?`lj5}8( zrUen&LW*rek(VO0Rglsa%A*wMYwol&)88{wrw|i2PV@ELxw+pt=ib|LTn>Hm@;LfF zz%}gUxcwZ*9VBRqW-{{>{`r`09=|=HFR++u3&R7o1uTo6#`>eIU!R{rM^Tr*)mL+P zu&*XxrzJEu99s|~>ruAhmHE&D_;rPOl~gPQmFPMo7K0?j<#bx!?yEX9tNBCsP9_o| zZ(1Izhh(XPIH-p-sLNoffoQn_(OgaMRglazkVQf6*ByyzzV&`FW8nbg;X8om9pHWo zxZY5FP4N}+5}54*W?rbdMHYvtu8?WIt?3M7+d&9D*T8zNgY{meaU}#pC7AI_2E%1A zLxqZzOkn8liL7sbRKkSFD$>idke9!!NEB6;zCrR&65|jTM%E6+rH=WM<;eKr(pQLqaiqK>gaawM z3kUVA?cdQ>#**3`GrcH#pxUC3+{F4$kA$3-QWzQ z;B>~Nx{AmDw1D@R$lrqF$+;cBy`21QT2J-JlUM%K4NOp7r|KqLCd6iX5t%kZptZJ^ zzx+f5QeXJ-1HfYQF7hnT>ke5aOHk_XtCX?Sgt%`i=(O@EgeO(kG1dMOjzLVQoHJ z?d-T@c0Q}vBzS_UJg;D*G#=@wVD|R%8Czpfr?I|xpH^3ik8`?9{);{2<#w0+2K;90 A{r~^~ diff --git a/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_default.ico b/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_default.ico deleted file mode 100644 index 8ccffe07944d8e8567fb8f3acc09b15dff902982..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmbV}OH30{6o!ZB!mwgtT)Q$Z-4hnX#F&W4Bc+y-b|`HMeFd1(MoOWHh$TQFwY=08 z%0r-3C9;y`v~0CPwe%pF4BreE&K348uf`D<*~^ z_p?m)NrpMYFwA)(5+acwakBmMibNt&jYVo@vpFr?S0X}>H5tvulrJumoco(uokbPb zRG7ZqXA|Rv&yJ-57XnoZJT8;KXG*|bqiEk*z+O7;qpi}P);jd3{KX>vRltLlfEzD| z?qR9_4hHQR7^%v{P$i2#n+P4o1fKG=PhGVki*+f(3U3bB=4|wmUN`55usYsG^Z?5v z_c2r<$6&c!;HeMI(L3_u)V11eRf`tR@dmhOT9MmrCTC5;*5WAECViy78#66Mn5Z}4 ziCrpq2z*H@6dn!VIp2f!xo&u8JJB%Zfu@tgLrWsogOBjcVYn96=P_(7h|F^7j31QV1`GD z;gvCzjW~pJr(9o$DrX<{wPDoMe1g3^gml?9WSP6jV0VxtCO)aFe66+MVta)tI;%;; z-y&?ov=^M;g2c3U80h_qI>OW16_p`KRNrS?I%%djNgj0VL87f(PZ3#J_o~rzASxWsKE23{riE30IOg;Pr-)qdgGL zUFVE|Doec!rD{*`I9i|faCD_g-tZR7$6rh`nLicfsSo{@AbQP{{?8rd4xjVQ3qif% zK$t!4)oc!d<3hsxvNijl=gBk81rPI&?jH^v_yBC-)Ne^L~Q|5S+VA;M4r{`l{^7A1ZF diff --git a/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_developer.ico b/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_developer.ico deleted file mode 100644 index 07bbf289e98427e71132ea77f9e1e8542402c8d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmbV|-)mA~7{}jH^aC0#(t|{}1rZ}6Bt#2RcSI%pRd=%e0p?)PMby>UL@&LZ)MY_k zcp7&7FOtb5>~=f!yZjF$5C|ZZO2Of9!0mP)W)wR9Bc6iAVlkxC>BEc-`7T?R zOT&DjbBYx@_l#JPNCfeCe8}*4Jfk-)%4=*x?vw5ir^yQPqETM=_Zd#76QU?091bJ6 z&0#6;VRCa1#IPFWEfvLjv8=p}Mx(FMlBAf0 zic$)NLTEG^P*oMJRtv$G6+9j>6k^;q%Im&W;wulDJw#tup=lZhg8}B>>|pV`3brYK zBTbLX->7lR^)3GCs>BibtRnSGL*!Kz3+qihtn6a0(ZiXKGT3|h`MCT+Z+45X%zsg_ z^h3k)u7==F4;RWxZ}Nk3?qHu$eY3{D(k!KG5-;ATS?Eqr=zY7Uva(>5+4pbNxRtb| z|I?j9#A)jFjKr^8mXx?&|Ld~V_*t4|Y-SpH@jU4?DNU;W&WnH6@?!TyUerk42|7uA Ql3owU)77UQyvU1x0q}M$Jpcdz diff --git a/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_media.ico b/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_media.ico deleted file mode 100644 index 50e357ca8be25c82e6d4bc4cb3af5118d83a835c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmcJN%WD%+6vl7CKcFkuF8vcMt}06DMnsf=T}c&0BBiN{)@svqQ7HJ>1QlDVP2xDW zF;!|8@c}{w-MbJ=GPT;&*rfBgGo3q#&F8sOg6YE+EY9$o!(7fc-{H*1SQFjN&5WM= z*ttE7?PZMZC+LVy*3PN?w@dFiKFqt1&gjv@wtoK5&UCfgq`6ZcNQ<`3o7E*qVIy!A z7NkNb%yk6x?wCv9UF}v~pXqF|eY=X)@+Z`GK=sIK32M$i`UAEkef)GQCI%wgqI&!d zdhKir=_{eGl)Z@UP~Z2L$)9#r_Yd_>br<@BwiL_A+&BqCkA)PU#8A{?C_L_7N2>1x z`7dtpC%tRpLGl6?-;dzyyStd1N#eunAuQwvh(yT8xA%~~-@CQmvoM`bV5K+--kPhXqLC>#5ei%lf=DGrs=8&GM4q+is&~5|zKY^%FmMfIsGW3T0>b<9K k^2?F3MsuhvY=ZKwHA30ddXC+$=Vvz+$hO9t{tc!54e5t#mjD0& diff --git a/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_member.ico b/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_member.ico deleted file mode 100644 index 7d68fa3c81aa70304ec4667475553019f963a839..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmd6m%WD%s9LGoS;4vr9UIcIc4ZZ70JShl1pq@Nb+n#)!RIsO_(9(lK3sO_lnB9>lq@67&|u`}YbSeR%pu;sIi zoo9?)5{Q(@{K|{&R7Nx!O+_LRgu`Kw!C)|mKp+60&j*jk1Gn1^m&*mG(+NdUh(J2kjUY1AfL}mePVwd zJ#7Q<-NSVE8nAZ*m5(b@PhM#Mze6HbRTul6g_G7$LT(AX#_^|mB`q7%y4+aDD zdOdVHorM?D)49^QQJ8!KpAHr5gmk=(7s8=2eg&v&BpXxtM0oEnLa?ZNM K*uOE?Q2!I-`gEcI diff --git a/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_settings.ico b/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/task_settings.ico deleted file mode 100644 index 619bdf265d0251a668e34a5bd09ece6c0d51150a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmb7DTT5b56h1iv^CmqF_%@HzuNVaXM-LU*OBZ?w6+gec5w6?Y;oK9!czkmk@1{!=mpLJwp#4gwt$ zVOqMoyRFU5%?5HQ?sw>C61WSf|n(DL~_9UmW)%jMEKJ3DRd?d@7iON-Ii z*f{+wMGU(Ub0yu2gJvSVRk!GZN1=H>jmySwD^ zcy!c4g$E^@%^EK+FTxks>iqm%)SPkE9E*#Kj`j6*Iy^k2bUICnqKN%HJv|0$Wazq1 zcXxMmc6LUrf$KCmIa$Mp!(j*f5!Z8XZ%=4^e0(Io-*3RDg18EOda+m(`B~@q`1sE7 z@UUZabX4f)-0Xq%&CSi}Gcz+P`lM7fLVMum=0?<-b#R`gr6pQjT@||78|!BMv$M0c z`Y|5+z0hE?Chp<={XMCwDm?M`!^4B{%e~{i1cO0yaB#5Jzst)@5AT0_dlNY5ETgU_ z*Mv1ClSy-HYpcxo*Vk8}0Um1Shc)25Ov9T2EgH1i;8)}G`1JI|{m{T+1BZt5V4&x} zonOZD07nBjtjJ>lM+t*?0$0M>v4F=KkH_Ere<}<=@8D8!5_~(U~5_Ba7L76MJM5rch zgsF1&2q~YUE1#O7%S(r)zY6Hc0Btx1y7>s`MlsNG7|^A`)~P^iW)dzA{6Og0G1|%R zE}L9c^vt72dmHG@0v_jp?k0D3oB-W+6u-4%(3(`Bc0UHLS3=ddfRK_AyOY<7!w#ws z#bI@{9hj;EJt7A^m``TT1>KcNW+6SA4w2qU(8dZ}P7~m2a+t))8xz@H#snY9%VaLB zb2GqPD==LQyp{sbbI3d=fuRamKfJyF+TK_ZRnm!k zet2g?JZFoG@N}UWH;3w!i@0jyW^gOd&13ZSorvT8JgimTlbBB?>4&=?$vgF4aziqQ1{NEsBsjWm;&sKD+VwAG?;tx>?w+7 zZ+F7IVu&)qJ6fh<32T>pMK{0@s=*YLTR4#!)t)E+JpoLDp$#U%1usuI@=x9- S!A}N}V|RK|+X74i2m1#JCXm$t diff --git a/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/umb.ico b/src/Umbraco.Web.UI/Umbraco/Images/pinnedIcons/umb.ico deleted file mode 100644 index b1baa5b622a303eeab112a0b8638a44fac119b77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17542 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 diff --git a/src/Umbraco.Web.UI/umbraco/css/splitter.gif b/src/Umbraco.Web.UI/umbraco/css/splitter.gif deleted file mode 100644 index 4dd2185e91380d33317c877ebd8554bd7b666d0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88 zcmZ?wbhEHbOl4$dXkcKtedFTKAKw*!vM_*v4u}BBFfi%&^shYqmVfb_Ew{Qi-`i8o o+VaR_+Oy7OuTE`y$9?>h&-HJ;&z~Ou{*RxBtNlpQA_fL)0NP?EKHqVn8Lk#c`lIrjj7P;QtIyw;Ol? zc?O;?jv*Ddk_A|pTm=*lC~zFVdQ&MBb@0OtiQr2qf` diff --git a/src/Umbraco.Web.UI/umbraco/images/Lplus.png b/src/Umbraco.Web.UI/umbraco/images/Lplus.png deleted file mode 100644 index 848ec2fc3bbaab6345864c303684ff8a86559cfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv*!VDzYUPT51DfSXiUsv`EJiLtR`ZJfMMF533 z3p^r=85p=efH0%e8j~47L4yFF5LY1m|NsB#ebqn)F`$y&;y6$pQ%R6t@PCG<+YP*c zJX22>$B>F!$pS1)t^$e&6gZd#)XV}^6#`leL>0XheB5$;*x1}$U0qr4a0#d>%#nkpi;(?AirP+hi5m^fE-m% z7srr_TgeFwOpI(f46|1#2yiGY`Di?KiU>FVdQ I&MBb@06e2A@c;k- diff --git a/src/Umbraco.Web.UI/umbraco/images/Tplus.png b/src/Umbraco.Web.UI/umbraco/images/Tplus.png deleted file mode 100644 index 2c8d8f4fd38259b2ef70fc63fad505fb0a0f55a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv*!VDzYUPT51DfSXiUsv`EJiLtRLI=-jc>;wv z3p^r=85p=efH0%e8j~47L4yFF5LY1m|NsB#ebqn)F`$y&;y6$pQ%R6t@PCG<+YP*c zJY!E6$B>F!$pS1)t^$e&6gZd#)XV}^6#`leL>0XheB5$;*x1}$U0qr4a0#d>%86hy4NT6+w`tp00i_>zopr0L}p{>Hq)$ diff --git a/src/Umbraco.Web.UI/umbraco/images/aboutNew.png b/src/Umbraco.Web.UI/umbraco/images/aboutNew.png deleted file mode 100644 index 94e13801670a834363ede866d1936b4c58b2680d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1178 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m=!WZB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxKsVXI%s|1+P|wiV z#N6CmN5ROz&_Lh7NZ-&%*U;R`*vQJjKmiJrfVLH-q*(>IxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8EkR}&8R-I5=oVMzl_XZ^<`pZ$OmImpPAEg{v+u2}(t{7puX=A(aKG z`a!A1`K3k4z=%sz23b{L$o& z6x?nBx*mr=^ns4i2SqDVG{b~|X$QoFCt4r}p6pZefN8x5n5e^Uf6!xKU=;9laSW-r zwIuj1Z)!)<~0a62ur9ls7(;O;ow%l$m{$gZheLOdREUX-d?9; z_wRmR$8}8NYrl@uQRDdh!qXxSi)x$>Lw^>&8cJ9cb8qaeJ@SjxIfZRzV=ont9Rtu6s;F0S}}qRi5L*LF+K zIn33aFOnK_N_3Sr3j3P9oN`Oz*-3?K-qi)>vt~5&=52qtsJl%0_7Md}6OIQ~DHZpE z`=Y+dU)Pk-NlNfh)mV1r(o~P+&h*vZt<9%qyGZrbT&e!|{i~|jmlszXM4o0OsQifS pXSAu}zL)&y=8tEFf0gtZ*%%H*HF3EY&Z!5L7M`wtF6*2UngF(!jj#X! diff --git a/src/Umbraco.Web.UI/umbraco/images/actions/sprites.png b/src/Umbraco.Web.UI/umbraco/images/actions/sprites.png deleted file mode 100644 index 8a3ed9b934e88ab3a1ac7c6b75629a865f32c233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12301 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 diff --git a/src/Umbraco.Web.UI/umbraco/images/arrawBack.gif b/src/Umbraco.Web.UI/umbraco/images/arrawBack.gif deleted file mode 100644 index 9d3f0ca6869d1af01a28961dd171ec57bc826569..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 834 zcmZ?wbhEHbWM>dz_|C)t1poj4hm#=wC>RZa5fcK6KUo+V7?>DzKt2ZL2?h>%238Ik ij|~eBHggCo{R#gdf4GrP-f0R);KHNb68dZ`4AubXbQK2x diff --git a/src/Umbraco.Web.UI/umbraco/images/arrowDown.gif b/src/Umbraco.Web.UI/umbraco/images/arrowDown.gif deleted file mode 100644 index a02ccbf6f88113a7b463dae1a297c8dd96b2eb2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 832 zcmZ?wbhEHb6k%Xz_|5dz_|C)t1poj4hm#=wC>RZa5fcK6KUo+V7?>DzKt2ZL2?h>%238Ik ij|~eBHggC|{nlQ^ zQ9mL`$s(v?QK8*dcWdp=?96;WX1u$V2Jt1&H#6US@B2L8^S+<*swjtqltxRyE#`W0 z?zsl{Jd-nK1XAjr2;!-Za6Z@Ha|iz$&u8Q=oB6@w@R#8GGT=f4UrTmy=H9n^560gf zq96DdssfOJ;CDKui6BtGeH9EL2(I99edW-xF`OHF^y>J(0#uTz5$9ivjW3@j=>u9g zjGfUU_Gg#j?%4ZNrP565&%K&Y38VzRRzuBz1_T9jdb$pSE`DNrpMkX<5;rC-v+}c+ zN+wZUN)Y&xf>eB5035!^-Z+t3FQE+!;KD)Ca=^Vh`Y2KU+4CbUdStk>v-5NF2vUfM zPqYyoh!PBfu*&FaDIr9b#8z}6XO^)h6GL><5bQudJbGp1LZMK|EqWjXu}HWA@JRMT zjUla-V3V5OECpv;v1-E(tn44a{P!tL%Q)&v%jV0ME?%irDw-;}Fc>KaspLN~2K6#j zpQA06MY|bBl~`s&QQUm`6t~ZhVEw=*By*H({zrE{pFcthI5;`xk&%vQxay#0+gMS1 z30gvlDs=1GRh&I>5*@4ikcdUFZf*imqZNh1r`;NPU~p2<>;RgmDs&u%89f1$)+|EB zmDsTB021jmm?h9=#t_nDuq-R3`JC&F6u6C!A`hw_5UN-4Il2W2hGrx6@7#xM^Bq{X z3V3Gs!{gu3N`0ovMjw#ObSu!60L)|xm8y;Mi*a}^m6Q^)ibTi`u#xuIl`^K^Oym3P z6nc7lu4{sE!*v40h}|a-Qf+a(Tb_egxQ`E(h3wz}GUL}_rc)?sYmk39iK*9b5jTAd z4sIRR6jj6M#V3fx!p$yfRzlHHuSLKnZ(#1-1maN>YPksa@f?a@K7h+v96Ne+TYG!E zt2A!Z$kox)wQ4P+s)|^;B&sGjs~fY$xdGc|NydcfI8|hOGWk7w_8#o+&Q3J;{SsgR X9tgLBzw#qC00000NkvXXu0mjfzlxe* diff --git a/src/Umbraco.Web.UI/umbraco/images/back.png b/src/Umbraco.Web.UI/umbraco/images/back.png deleted file mode 100644 index d0ab2e28b5c3d1a1ca487b3cda111a8b488903bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlMCm9|n$4Ua_ z9(cMqhE&{2dce+T=I7^UX3#CK&>(RjhL2B8uS_m)0)qnsBdf(siJ3bp8XM-aYA^^m z@chZ~@$rd~sgY4oTp-oJkn|v(&8zssN5dwT1}_Gtj>cZ@GwH0a6Q4h0=TKntixZgN zFUVmrW2WQ}Mxf;E>D~NnGb$s191abJYX=#bY;1V`FfblDaEwnZF(vKU!-t894?sHW z#1*O@$y09nXht;x5{fj&wF_>sv$e5zyBE-hn#Wc}l t%8V0iiUJWi)AxQ5)n;av>vwZjPqg5g$l+i<$%BubuSQw8RX~x!8UR(mSYiMG diff --git a/src/Umbraco.Web.UI/umbraco/images/c_bl_label.gif b/src/Umbraco.Web.UI/umbraco/images/c_bl_label.gif deleted file mode 100644 index 1f55e5b558aec2344b7a49fa423dad066c8c2d09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 948 zcmZ?wbhEHb(4S}H<0y-dP zgYp6c$4myz2nml33670y!h9|(E+`ysW07~-q9B-bv`t);t0tjw@$o(d`4k_EOAn7U zt1B;y*)b{U^dwf+5R(Z3O)j%c^R-$YFf3!`U{X3TM`0lY!+dt74javbj16oei@c_K zZCDY+#LN^B(4gSh)Z{zWOhG{50Mj})RTj~1)9kYw802g+E-YBc!p6tt(!r3hh=r4# O&%(lTp+QRngEatn=WZ1M diff --git a/src/Umbraco.Web.UI/umbraco/images/c_br.gif b/src/Umbraco.Web.UI/umbraco/images/c_br.gif deleted file mode 100644 index 2674d46df554b311b4b2242a072d52f90e28555d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 240 zcmZ?wbhEHb6krfwIKsei@Zj!$|NhOGId%X3owsk_nlfeLgr1&-b7pVYxaslZTZKfd+p)63hpFTZ{J zbpQUH2M_LEv10L*DHHqqdloHPaQpVH&6_uMb#}D1wk(`8dqPjo|NsC0{rkr-3PwX< zsD*&yPZmZ721y1Tki$WFf`Q`+gFc6hh5!>QJFf(f!GZ<_$7T*uqY?{&q(iN|nsFO6 z43%B`Buw*cDhe05O;EF+^rphm;q-K-fT|LW;FPnIwVfaJtO!;;KgFb&saB&z?c#hc zgN&L4#zRbuoLmAs7KFS!z{#)RAt0dW%(jY8-p64_!huF6$u)d64vkI?-RygUa&DJo zGcgxk-!}Kdw@j8T@%lb9E_^!}y}M#B-@V@poSN6BF{VrWIpD~^!7HZm=k?XqPq}8O z8Z?~RS=`3PppqeGpl~qqa-3vOTJ80Ql*?;{cWmRm{e_WvTmJWR3mO_eKHR{}#9$2o Di-wo} diff --git a/src/Umbraco.Web.UI/umbraco/images/c_r.gif b/src/Umbraco.Web.UI/umbraco/images/c_r.gif deleted file mode 100644 index c3976c6282cd5d4f4ceb63126db58a44f8ed8b49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93 zcmZ?wbhEHb6lGv!*vtR||NsBLeC6!pCwJbyeR}ZV?iDK*Pnj}t(V_)ytt~()uo48J g_>+Z^fq{!b2P6bCgMo!hU}U>Z0gLM-ng9R* diff --git a/src/Umbraco.Web.UI/umbraco/images/c_tl.gif b/src/Umbraco.Web.UI/umbraco/images/c_tl.gif deleted file mode 100644 index 836446e39b130faaf123cebd4f3ac99d56cfe586..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242 zcmZ?wbhEHbJ`*! z!*}XYlh8-0Ez=5bdW(6U_`u$pH23yXf%v0#^$rS3BK1uIb)rfFqFr5WRoy*ZO8q@N Z?PC2?`9*o`c;?L$<%d;Iv;+qdsNfBtyl`0B znKE(a%$fW5?|l07?$4h;Z{NQ4?c2A{pFjWm_wV-Y%ZnB*c>m$~%U6&3`+L5A{d)S$ zvG3o%fBN+D|Ns9C*l@{j7$Wm0)=*dx16w?%9;+y2TaM6-w;+pD0!ZMxl8jBW9U$jMvf1a|Z#<62d zj%`s_VbEmVbzDO$M?vGX3g3Bmg==>;Jn!B(SFft|{JDasmcor&7oNR%p7Z83(-Zek tpLL!;zWq$@$=c5e{PGU`9JU)cU+~LuGOEbA&GC42q_bM{6e}ZxH2}a8%Dw;q diff --git a/src/Umbraco.Web.UI/umbraco/images/collapse.png b/src/Umbraco.Web.UI/umbraco/images/collapse.png deleted file mode 100644 index 9cb8909df2715dfafb15ee92d8d3d2eb93c5aa51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 388 zcmV-~0ek+5P)p00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz7D+@wRCwBy zlCf^WFc5~5KnP+e8!Sw8X;rE13_KFg(#Po5H(;b4SU{D!AThM6S}8?Hj)QZyW7BhN z1%`;Gr<3)c{hjSQ+kc#`US2m|4IABFdeeueLp?*XS!Y*7t!NfOex6pj)Y^A624_$y zRR*b2h6!wZ>WjrXY;_nkPzxb4NEoA(5J#Wda=95#?oo6vBB@$?hUi`Zx2r)V`aECR zgs9yInwCINRH@KQlq7sKzAF=YgBvX+x~md+hU3+BKaM|C4lI3VsW%*UzQ5Kv)A8pU z7vk7KECNS+Ijyuiv=N}@U^Wt2a-{KQ+!UWwc96iK)P|;HYi4&pf4ynjy*w3u(Rf}- ig<)ic|F1vzE5HEz7TL$?rb^NP0000LG#kBaSCzVZLZkDpc?`hW27 zi66iJ6;3+);QjxjH~!Dx`~U5?|M3|!YnIzZ?0|O(g%gmWGmr6)V zDl#!J9#PO&V|}vO=!7FH15=0NyxCKyPMyx2G@)aU4M^#an>UX-+;UkeVd?UO5hUN< z*w`4&bwr$9l10FQ=aUc5n>TmXDA?_9QCJMtaO1{}iXVy$8Vn%WO*wDg*f@kLFo=L% zktrr7Ho+3c(^E)DV%C1Z!o|QMzF8r>mdKI;Vst0D*AM)c^nh diff --git a/src/Umbraco.Web.UI/umbraco/images/cut.small.png b/src/Umbraco.Web.UI/umbraco/images/cut.small.png deleted file mode 100644 index 9e936845975eca508a6af6255653ece236d2b8df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl^qXECGJL`I`IRn618r!2~@SeBSwclyMk=FZ8CcB|K}UAJKAc1H6>yLKN~yJ7c% z{d)qF<}6yWeA3KK{02>PmTYS1oRgZ@VBu8~o7MRL|9}18iRXcKDU<~H1p~SG07FcS z^jo0XOP(%{Ar-fhCb%;*Fdktz^5{{*G6v}m77m384GhNm9Vc}dlo**fR1Taz!>;mE z;Sd8Oi_nDhiDFa5pC&P71C?!Hkd~fo!!Le=qq2d4iTlLq4<`#wHI$xUW)W~{@Q>>2 zwV&^@omAp+FCpn=1h*?PN;xixb;M*z@>18D}E4tWAC hPjdKf=g`2&koiC&DES@VQK0V_JYD@<);T3K0RWk}yh#86 diff --git a/src/Umbraco.Web.UI/umbraco/images/date.gif b/src/Umbraco.Web.UI/umbraco/images/date.gif deleted file mode 100644 index 8f73cb39a6cb62775bb7f9b9e625f87b79f9c6a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253 zcmVhFn1O(RrJs5>N;Y!lF>P2#6(4r^8?}FqjUU8)kgP^w+004+M90xNXfhe?c@(f% zB$WpyQ@LaUCIS}~gbErIeo!O;6dDy7h#edpb0ZuF2$2y0iX|HX8XW@zW-6wqCLsVj D1txDg diff --git a/src/Umbraco.Web.UI/umbraco/images/delete_button.png b/src/Umbraco.Web.UI/umbraco/images/delete_button.png deleted file mode 100644 index d1d6a6413568fd9f1fa4f971500d32d1bae1447c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 582 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 FV1n!}VO;hzONwV zi=~t{c@z{BkWfdk*=%TRY(!E~us`|~SDSA_Wh}+^_7}{}g`n3Lkq)aQW<7XOCYWh>{=(SXo(t)oMK_c*ccflBcw7;o#sU_Vym4tgMW3AH(wU@~JG#ZiB%PD=#nC zj*N^9odZxv>HK^Z&dyw@s8GlWj`H6^g6T>B9K98EW+- z^z>w%`vc(hdecJEjg!bq0TUBIUEMm-@*ornT?p_?fB^swfePtsItefU0000qRr diff --git a/src/Umbraco.Web.UI/umbraco/images/dialogBg.png b/src/Umbraco.Web.UI/umbraco/images/dialogBg.png deleted file mode 100644 index dc8fde7ce69f75e35d0444c0e901eaf30ccaff0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^Yzz#ncQ}}UtUlw6k3fng-O<;Pfnog#bJnhxK)z&& zYeY$Kep*R+Vo@rCV@iHfs)A>3VtQ&&YGO)d;mK4RprVPME{-7;x87V)fytbzug>fddy7tk`hj#)ktZ9vryvAwIY2!3u!| zD-KMk2zao;KtVwvKtUlP!l57{AR)lv!G{kU4m^0ULSe#+12r8}5;PS0XDpo1QQ#mU zuwli700)Bt1BHTwfPesp4;us&G#vhe0RthR_>+Z^fx(192c!_>CkD25hgk(4I#R7n zGXfS`C|Rp6WqK-dBuy!mWz$D1^~7(gI53m diff --git a/src/Umbraco.Web.UI/umbraco/images/domain_on.png b/src/Umbraco.Web.UI/umbraco/images/domain_on.png deleted file mode 100644 index a9e7204fdb8f4f8531c7f2195f57d088f96a67f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlfCD=Y zC45dH%f zJ}j8guwcaoE*GxclB$rsAq!R!iod^GZsGh@L@tl zfP#WTfP+CofWw52f`WvAfB**t4TrQdY5&2X&3`q>cLpUve!&nCqDh9Nr57rIdMA0h zIEGZ*iaCE$s98aPC4i&tfmpfkl_PJuEk6CPpDrl4`jYsncwc#iSNccx_-Gyd$;IQK zH7`2hLb<}{!%R2kzG5zF)crEMu!JclbB6V)YM~_SyRA&D`YkW}w^hA7$@FPu-)<8NX5Z#1H%h$_S)ct`IDz5hJmtlU TIJV0JUB}?*>gTe~DWM4fzlh;1 diff --git a/src/Umbraco.Web.UI/umbraco/images/expand.png b/src/Umbraco.Web.UI/umbraco/images/expand.png deleted file mode 100644 index a0728673212690d37d80f249886ab21a4e8be66d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 400 zcmV;B0dM|^P)p00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzB1uF+RCwBy zlD$sDFc8P%kgA`wsH!rSp@I?76?i0`g~wpy1u7P#ek>IUNJub5Yzze^TuS4vX#Dvh@1P2x3_mZ|1}^|+V>FAv&=M3#LkIu5^7_WSd% uJ8hVok26#pJl#pvYIPl;fBl0S0R{l^iqX6rNf|Ey0000DIO(Acwl&EIXV2@^gz6kJ$v;D12HgN7CV|Np=C|Nrg(|6lo5uL$V) z(7ozJLdS!Gsk{ID|DW8w=Kuef$=&PzJl*vF|E*s?AOGF%_UBxUQ{lXTjt2$_8yXgT z*mmm0gf;87p8H%dmzn^R3ENcki0M|8KIyQ_Sc8)9n_< z>>IQX%zF3mr4;MsPakzAJJ~*cs`^P}o2UKi#oIJl;u*utj1vsrd^_HxaKraYs=#gC zKE)t^r%<-ums;Tl(>PAH8#iA3J#C)-Le*0jPoXWq@QyWyK~0>iFndOE71-CKZ8W$<+Mb6Mw<&;$T9v1@h! diff --git a/src/Umbraco.Web.UI/umbraco/images/false.png b/src/Umbraco.Web.UI/umbraco/images/false.png deleted file mode 100644 index 561460d601a5646a597149bf2c63e187a63d8e89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 894 zcmV-^1A+XBP)H{e zgve~t#T#qeTi=XX){_9Ek}qU)^9w(f1NF;3=K5+stp*&`L}qjEi(fy}*S9IYz6%tK z0|aoTg5{Yzm_Bv-`t^%{e6&aLua5zSO=EYa|J{*wTefPx5Xdc-kSi9Tm|-QGk!ni7 z&@|k;aNZmH;m6&3rGN4f;47k6GJTmdZQUD8+pS|_{6=|peByhj>RzZb{AhDy=B@tz z0ZZ4QheEh_bE0~7?A)uLGGEpKAl;ch7-|Ta*_kY|xBi^U&(3}D3G>yJnp5d?FXwV7 zdJ-N9gu{_&Tl?CB0N$oA2rq2y?z&VZon+uU_ouV3?PvZC0B#eBaC36F(9t?5WgT;= zl$&=P>Rc>`paTEe%v_JI8~V$3E>8~>!%#T25-T!{rKVY~Iq0C314Q8pV!a+(@&D>iPMhJSXcwD0heJK7NN z!~!eWbqSx)p}T-*3&O0F=vKl6fSOLjHBtlQ`KX$vf7Nk@jxT%106^+`bY3GEz7GfR zL_JV<+H?&q0RxG6=0GBRG^uzHK>$x^{*oj2e)BN=F_EUlBHI&6p-~9PfTwlG3(hGa z%FkGjaZOocpi$UxzNA Ueb)Qkw*UYD07*qoM6N<$f~nH4#Q*>R diff --git a/src/Umbraco.Web.UI/umbraco/images/find.small.png b/src/Umbraco.Web.UI/umbraco/images/find.small.png deleted file mode 100644 index c3f3f42e2565cfa2ae93591bfa3f5fc8c6791f1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlHQ?;;f3qg7 z|M>6!?~}bVPQP1z{p-dpN2V@0Sv&dYzr~in_E+tkkq^`~?SW@4kP;{f@(cct3K$+J z$4Ua_4tTmahE&{2deF|`$HvB%=cmA|&>*2ulb6Tm=2n)MS6A1-(8N(tw0V=!recGl z;*ZQM0xk`+=Sr?%TG7@huh5{t#M2|wbH?Goi39us4hD>63mCMGjhXEo7!Gb>2;H!0 zQ<1?3u%7POf;tC|9s(*#U^Me%%UdAFr?+4$10##jgQtl}51A}`xKA*0D6kpK@0={p z#=z9UcqYB^^G`;mjz-?8=YiUIYF5+$U6b^{<>_ajiOlv=3>*dw=4xE?cJ7Q30{Vr) M)78&qol`;+05ztOs`9(+SbF*YujcY zJAG}+tffmg9b33|-^q)2{bKT$?z+}7ZRM0jTelv*=9gH}vgnvkOn%F(ty5MVGxSW3 zNG<;U`~Sv+=eF-Vvi0`&DVvUMz5Kjo>cYtU+Ob@VarmW_^Pet4J|F3j$Qlw zf9d1z$6kLw_WA#@-~W&O|9|cO|L6bzfB*k~?Y_Mmc5J`?;O@`gpMU@PI%isTaba?C zuGi9e#Xy&Tc>A!r&}-|WlhHqcJSUPJ8(8nO3kAe{&0*XIb7#SGs7<51(0m>5$97`BPIYc599eL$B zgtaCdnvmGY%P#zD#f1qA+1LbBrB-OP9%}0sQZ+fk`0yYLlbDKH!i>j<8HDwmCj6Lq zf$^|3tHikp8y~qGniG`MvA{8zjYouCC@aCj=}6C9eTM=MkH*AK9>at^2@{hzLp5a= zDD1evNNA&#O4l+k?x+vp907ALaRZnVXvPH|Oa>)FQ=yet|50 z!bYefcglfiT5~=09^8leQhj9fjJ1$f>YRv{=JGcmKi5=@UUKAF^w)}MgURRQN=Y*p zsL#NfZHiwSU!hr@fBOODu`)JzWccU+vIgZut{j*k z4T{RNmq=pv{59-`?>|Dsx&~YKVCn(4EC?T25&v#;gJGSkM||uk{ZR3`^s6ss6L+qT zQh{Rpkp{;xT6f$FCklBbU$im)0I+igF5bAR{1spTWQO;P<6#c-00000NkvXXu0mjf D2G1D$ diff --git a/src/Umbraco.Web.UI/umbraco/images/folder.small.png b/src/Umbraco.Web.UI/umbraco/images/folder.small.png deleted file mode 100644 index 7b6835d04100e6ea4d488a0760b72ba71b353144..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlj%?@Rw*&i;PA<=!Tz=lg>0ZL++#&FlZ~DgS?e|Npr9|MQmr*R%J|GyMO5 z&YVh#t$h;z|NjS?=csE8a*||8kY6xR1S?=*G~wC~RJ7UC#WAGfR?-9qW(LM13W|z6 zEF6pt3`r9N#Kgs)yLT|K2sCfl!6DGm-N>LFz@X3|(Gn@q*x)I{;=lyba7Im|jYWll zsl#zbq|Y3AmIf`0pGRF7L40SP6b|c-1OY)|Rt^QWL^eY~JC1}$Y+OJiBOC;nZdBX^ zDdf;mU|>8G;~>hw$RZ}t#3UzX7znoCnQ;+N=u5YT(F6vd{*wDlECOu|4h@V9$t*ika1(h4+o_;4!#VGM-K3Yoj%Pa zl$ieL2|I_v0tRMz5d$MeRt6@H$cEm|nav>UB5pLM8E)L%^wWV6sQ*Dao8&QuhsSA`D4Afp7WG*3*Rh; z3xHji`;f{Qr-YKUcE+jR?efSretxx&WDEZ5Kp6he@Ps65>d=M!?SL2nk+yO=?eq*e z(CtEpt^NypL5|1sYZ`?lUAUkN^c5N>XnA85@Q^xwjeFYOp3C#}a zHn963=}o`!<#;@v?ns|m4?LBXgiB1FGzEnM(t|}KyE^*qfGRmEB%Pk}RCt;_Pv;q; z76HNqq0F}lnk-J0#Dba+rBbqa0A~EtaONtWn}_d_M-ZpT2;?ji3JStqMa?@ZmdPX= zTq#gV_Ido8pfaG-T2k$Jy;cl*0_LLNB}Aj*q>CgnRRXGH^eBHh);MQI_PI}xb1tX|OddyW7d8doF(D4G0y&Kg=U1)lrvBkRdEgAn&YpWK)7FF_ zO`KDR+SUr8RB?R*V+siUO(){p8Ev7H;ZSCO#!x@rpA5_#7?f@4^r^(u+m}It5>W*D z01$DV!W6U#oGVnbeNyJZnHA9JOv$#X*T91F^U_IYN~VH2gffuvM6+8egCf_g{|0&7 zthJt^rtYhj1}RrjRxZ>}H8ti;eauI>>0UIql?R%KiPs;mGXsZUZf8&wEjF3bfvvMZ84)BX)l6^Il7Go3rYTLQ8s716vRLh5+D9^vS}hjT=Z5*S35 z5hDD6A!QEqGnj%>)-N!&3h*lxRQe;yRi@?+enk_n6j7*DCSFh4__kfqo8_%Cq}0A3 zC2Oj76@t~Eogb%n2;4$a)$^vZmg^dr3uqaJj_2bU3-~2QHI{05h%BiraW5yFi4g{M zo>piBzZhYM`U?{0zQ)0*L1~;ibKYpws4-QP7cP7(%6y^brj>(CJbsn8#=A_mO+-{|m9l9vi@W f?D3D|j{pMzZ4LyvY9Yo&00000NkvXXu0mjfjf`b8 diff --git a/src/Umbraco.Web.UI/umbraco/images/gradientLine.gif b/src/Umbraco.Web.UI/umbraco/images/gradientLine.gif deleted file mode 100644 index 2b359cca13e0d215360682a3651579658e2110cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1482 zcmV;*1vUCdNk%w1VITtm0QUd@;Nala*4EnE+Su6G)6>(?(9qA%&fVPH%*@PCP*BRs z%E-vbj*gG6uCB$z#iF93!otJA!N6KtTClLNyu7@$wX}?kjB;{vq@<*Xh=@{BQhIuN zbaZsOy1KZyxVE>qR8&;5va)4mWnNxhf`Wp+zP_lasC;~UtE#JHWMrF~nx>|vmX?-q zaB!fYpq!kXczAeLR#tv~eujpIZf+9+1>Ehzz|NsB|{QU0j?%v+s_V)Jp`1t+({q^gDC-=;-M0?(XmJ@4>;rZEkJ)`uhC* z`}g+tl#`SG{r>0Y=jrL_gwy~=H~6}?dCc)qM_^S?5U}#!^6YN%gj|(Rj8+@R#jC~R8)zHiC0)y z;o;$P*0qkoBI0umzbE0jEh@aTb-Sqn3$Kx$H@Bm z`S|(x`T6?T*wFMf`k&)}{>~C*x>g(#{<>bi6$mHea&CSi`=I74M&g|^$+uPgo z^78HN?Be3%A^8LV00000EC2ui03ZVa000R80DmwN$f%$}Mm-pn>9Mef!%7f0Rf?9N zqQ#2kGHNWRp`*u+rb31UQ4560ld?d_q9sdKj2OjY%8YrdST7wqc1SPgGAvep<@BiOKDDN=|SOSbH~vuKyC1M;n{+eVGV z8JP{QLgTR4Ph=yOk^X9b}+~R`a!-x@cSWK+3V1bz`qk9fugn+UC7(bpGHEcW5r$4qsg^F6I zVxfg7pn%AR8~$>{hlw;G;)t1K zGQo)_ilJf%ERy0PCYV$Z%8WC9^2rCHyiYmHLV5E_$oRUfjOfuO_ zGZ94a#*}aLVI>3*z@nv=4mbeKmtYPtL^j#jv%r~XhM}ecY_b_kES(@ACj$83qYpj? z-Y|+E0W|6;I^Cc%z@(G@VW}npWSS{I{Gfu$0SD+I zs;FCBvc(ouq?#%@0JMtBt5?LQD(gRZ;K7F!xZ+yRJ!`Bn#jn3cl8+sBc)|u7cidsc zvdhAwh8mF2Q^mB?PU4C@`}iY?6Wn&I#kXy=;f5Q=Kw<8=?Yxsl8k{WQ?z>C$bIuuO zpn=8{_~tu~Bj$YZ1s4F%F-H>w7u>`cT_D`WHfER+#lsJ469qT!#1V%gWK2T_6C8KU kgdB2YA@UPx# diff --git a/src/Umbraco.Web.UI/umbraco/images/help.gif b/src/Umbraco.Web.UI/umbraco/images/help.gif deleted file mode 100644 index de8c2f2492237e2ca8562473103d54467ef75985..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1013 zcmd^;?N1T`0LCxNvT|;ftM#%rE1Ra3S(eooiQa5w>TEg121TXS)*H3;s+pmsl@du;T@dFo1oIA0#+tjUix?R`}BN$_WYg$$@>#`3c`Re;Ma3t z8kS{=9?PhI5>yc;+D@9RI6+EYE0hK=-s@L)n>y5l*~%2q?8Z3p+ zZ_)az-gh?0$&YffOK)+wm@w{_6-Sy@A8m?EZ;a!s6Z7=jPIhb) zfIEcBoo7_si@M|svfQdb+O@U|HNVnDE!S(Oa`KV9V)#nA{?1G5H3{}Y@mJdTO;Sg_ z`h>|KJE$9C{49dfh}KB=_EpH)&pOt|uxixu1!F%qL5we;(dsP>+p5N!6=18{03x_| zoJ9$<)x31cs$Gr3XirO9hl#ed!)Pa>Q-Dx&JEYW1XKgD4qeehZnAi7;`+7iHYja#H4{b4kIq#@a{*)jkUph zmu@_e`j+9ip`cy$T@ATg_9#o|&f_JO`)Vs96V_#x_}&&AlZGU(p5TWg3#E&rQ$)C6 zucl~0l3TXr!Rh-8mTb?B<-`|tZ`kpa2ranhQl!SbM# j;FHz4hNz(I>v5^UA&a>8s>gjb?z$&$4u{ST0(SocZ?Dih diff --git a/src/Umbraco.Web.UI/umbraco/images/htmldoc.small.png b/src/Umbraco.Web.UI/umbraco/images/htmldoc.small.png deleted file mode 100644 index 61074a4d94142e72c0a7d6a6e2adc3e4d6b7c693..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 507 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlOAb|77RS*>U>Wf`bo2LR!7sx0@_| z>XzB5=A69a+N(29|H~!JYF~LLa@wAV$j+Lo(*I!4&-SboXqQ4skY6xF1SiQ56C?c= zsP?O;i(^Q|t)vOlr%!KUVpCIKS7?xMU@8(gaDYjKkwakx!)6QR#TLx^=96t47#dlZ zI6Cyroj-rRqnJWN15?wbnwmFnE)~hFIkSgn69eNBh9}+~8#Z=!aGW+Y{`~1Q6N^Ce zo-H=rOBTpTi<>iRdoTb^Z>T9V)6+A{(@SIwTfpGJV94F#z>%;NUkm?^lqPp1r~XtiNUEj0U9`8JVP>Idk?nfowf+P(fcuQ%y~c z>q#>U15=0Nyos%?!2tmclZBNS1RNBWpH^UuY+zvGJaJlyv4Me^!S}nc^H1}-4Zr|k N@O1TaS?83{1OVk1(NO>Z diff --git a/src/Umbraco.Web.UI/umbraco/images/importDocumenttype.png b/src/Umbraco.Web.UI/umbraco/images/importDocumenttype.png deleted file mode 100644 index bd898de94b1659470a34a212f2ec3d63f153eff0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 601 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl4}QS@!?`?Su&*dX}$oD7diT!2iG7T)S7DXjt(-q2s~-|E~(B?hdGU z@aO-3-|7{AE;I&oeE5BPmO;XXgb5EEDlYsvT~n~&gHz$WfQ|=$&Nlwr;`#soorVP; z7Vm%e=XCksO|B)=Hf=lg;`g0-|Np=F^K_F_;r##q-~Ru9ub+<Xne6HXzp>*o9U1wkaIal)^X!=XV5}>dBN`m}?fn4eVhBdyw&jC%I z?djqeQgJKh{86Dp1`Mtj6B24lOj@b;aPAaDKytj5+xBd#Z)7CH9G}ipKHCbM*srsmk`}snh)4fyPy=&!ORLFM0&t8FF;oRT9 eUl`siA7ID`({-%wy1N(XOa@O^KbLh*2~7Zk(OxP5 diff --git a/src/Umbraco.Web.UI/umbraco/images/loginBg.png b/src/Umbraco.Web.UI/umbraco/images/loginBg.png deleted file mode 100644 index 18d3f1387fa20952d01aa7f8ffa7a016454a8836..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17566 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 KBbFOFM*Uyq{^n8u diff --git a/src/Umbraco.Web.UI/umbraco/images/logout.png b/src/Umbraco.Web.UI/umbraco/images/logout.png deleted file mode 100644 index 97d15fa6b66ef5864b7400ff6a6a97794e5e289b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1011 zcmV8 z(vi>l`i@%~8y#R6Boc|$;TJFF4mCAJMn*=I_V#uxEG!_EN`bEyU?^#KcNZNU9Zg-` z-PgXna-~lMT7+_$MX7|c!=ZfY_eb7Gqrrb-u?Ljzs~?legg_v0;{1gR-`CaD z__etl#B3Jq_BP5Ca-jfO5U6Aew$0{09}W(F{qwVDZ}a&)xN3p6wl?R$jT;Y*9OpNZ zEFA^2xe0}eFqsT^x(lnTsD#51H5$9;>{;*B)YSO;`g)o(8jb9CckX;69XuE?CKAx| zJoLwop+vy_l@)-BaMUBNs|##Aj`H|81c$>eb#@lV$HsnRJs!{DCa3dr4Y6>Th4oB zvYMVwAKAu6q^R3GfgJR&nVkh&Z5lbo6Jed%?BE%eWgyGgqPOk5dCZhIsBA(&V==KaY>@-9yXWyRa-TquAMr z;>C+uq>va^9Np+0L9hD%j(_xL~V~&^?e7@1lTKjm~NFg3)N|cl{7?h;d`e7-R3jP`lJ}Ap_ zG)_~wN%Np?qe24pdOfB?p%wGc(6@u}_zyk3y~j-r4Gy_fVp7Ru>}@bOCs{0uA0iQ@ hwY62nRL{Qx3;P7$n diff --git a/src/Umbraco.Web.UI/umbraco/images/logout_small.gif b/src/Umbraco.Web.UI/umbraco/images/logout_small.gif deleted file mode 100644 index 67182329f2aa090b27d38fff6b64d446647fca8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 996 zcmZ?wbhEHbL-H?mbQOc$)2VH%R|gkksQihi9p7AJ@$J_2mAqyVu{% zX!>^g;ID_be?7Q$KS1M|uhh%x#5a>`-b}84oZz&_ko(Kw?LV%cf12X-e#yjF?fFlV z9AD)6e7|t~d4ABNFq7wb0ec}&Gft%YP>;%{dqy~&$D}8HfFqP%-U=t_i4lY z-A24m{1jdU$~^Q}+-D>HIM(*()ziCdq;7kuJP6Z$7^3ww!R~E+)SW=RtIqO!eDt43 z={Is7kmOsP{P9 z^4EjgH=Si)6o$R&EqmlGeK$z^d79(1+~7MA7LTo2o@IHxYR=kZCH;0<)8k0<+eQo@ z7Iwd^P5FNQ$jfZ!2VTlAl1$!|`hHk4;ju0Ii}IM~MWK7m1fQn3zMIvt%T@DLQ|60I zhdrL!-%cNRS)cav+}#1<79T_1rPOiL#)u*RNLP0CCb1`kHP%6~jFF0r5N zb?AHbpwlJcte{nsh-^d1!bvlQy!J?VIl1^ul;@XWNCk7RCwAH zk;`ioK@i5Pt7m&=vYW+MVn~!AisZ5wLB!xeC4zX+KoInx2>t_l3rKDvo{S#VlOU)l zl7kRL5D|okips)@9z5v92g&YcUfn(2(`E0asfMqrzWRQQh`RT#QLv=t#0Dv4Af^)vd|5+(t9N85(Q&xUh+IMLZ@0$@* z`!R}1E7g9Nt~~Sw;{gxjz-1{|dR{5#2FC6sZ4);PjbVL89sZ(e0mMZ8Z!z>NpLoQwOn=o zZM}R$>}h4ocWOKLZeF|(2=d7i1MvB?pYtsSjD<0VVg2}gG(CgS5b4~s-xg1opuetl z^}PsLXLd3G#~V$L!QbSg;@-H<2J1WxV|1=NUg!BfKfHr^{~{bP8}K#USw@zOW!6Z8 er`rAh0t^6_ZU$>R@+KYt0000{jOkN^7l@c&GZh~iHcMg|6U1|5(JkXi<2 z3yocOtPH$erWgmmQTGg8WR@(l+@wwYCU-J%bIhAarZaPJHFj-Z8*mwuMNsU O4jESS{d-Lq8LR;h)iKuq diff --git a/src/Umbraco.Web.UI/umbraco/images/new.gif b/src/Umbraco.Web.UI/umbraco/images/new.gif deleted file mode 100644 index ef0178cfa48c056e4659c2f586823b18fb2d67ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246 zcmVR=uD}2o(&5bkx2hNb zSVFP=fsJhyN&Fy3rVgMK&~l$Dl}A{?5VoSHYEperE&JExp_QUCw| diff --git a/src/Umbraco.Web.UI/umbraco/images/new.png b/src/Umbraco.Web.UI/umbraco/images/new.png deleted file mode 100644 index 0ee2a7d79efc3389e202d771b7327ec9728fee77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1035 zcmV+m1oZofP)u{|D~j-MrsUi$x{_3Efwf= zyI>TUws$Zzc6mcZDyq~ex-52> zO-3B>DXiDGuu**pugg7n`Fz;DyuAEpT@@wCGUfC5u8fDnpJs$2I8sHvQ3PZK6R%ys ztY;c_)`F(e#DOM>uaCY3-z#0a`|;g7CcTNKydF>Q?A+X)vT&@+;~2R*hwMonT$4jv zY(wB%NHt#}-b|n(SFnkf`0mw13|t(%?Q}Z(sGHYsyiOPr=ENF!r3F1<(UcoV)iNlS zO5pMwxLOuXu?nr+1|bqwTdUaP_k+_@(<4N+#;p+83+*y`%w3prjUiXgqjX$?P;H`m zQpOzK#yjRon000>X`kZ0^bL~YA?fYubrV7pT2w^puk1QH`A$qaFTzPXK&u9Gg#u0+ z4RkY|Ft7$#bvDGQE$Es+yTL<}jf8MHE^TsJ>(R`HeLs1CSUCoY5z))qu%MZRRej*H zehDe+0JOp)A;n>*Ot7>%luD(NrG9wyXq%SFme<;cdFQckMADtZ^s{g7`zI*sZT}g6pVp|iPghqu)%I{ zpo19UpiMBc6fBYv6T%eg$&(+`*>qgnYVp$E-d^g)yB~!7{lUO$x_=U(a{k^QN%w23;>FQyHGcgO|1X`002ovPDHLk FV1gtO`K-riQ}i58amQabLt!W>+0M0A2@vJ>Wv2vA3S^h z^xgY6PoF)0_x|;VkFWm!>-zt{AE@i&#}}VIz54X&^~aB|K74rj?%nf`A78(J|MLC& zSI?e3eERg^qeu51KYsB4PtpJ1<^TV7|M|J@|F?|)KPLSDnEU{C{El_nG7W z*S^2scD{PF;{RQ#{|`0(KM(l&wCDZXhyQPI|G&cX|CZAKTblpx8vMU!`u~yppNGl+ zpXL62mizx4%iH~|-%oP?zr^$ZGT;A8f?v)^|G#Da{yVC~cdpu%SM!k?aB6%1trJVG9AESB>Ymjrm(FU5KCz*#sWkS`#>qF1ExmSh>Gcz9=1y+d zyLI`q`8+Vtm-eQ{1e`s%F*d~W;GXTv7R&?22quL7?KXzSTB-Tln|)UWo|j? zOX36rKWVd)J3l@KJ!Y4%u6Q@+XA{f08A_^dM=o4^%+@VzQ{b^u`8X51kypqP1|^m@ z4gu#)98M1$7c@J?&S2S?sMI8A$R<+YV)25jS5&|$L~(-S!xl4_7@17RL&xXva{aWK zY1k^DprgNnBOoKKa~cz;70azlF8-}8nTKRD6h0iBXId|(ykvs|^C2#IgAy580S0RT DzvE=g diff --git a/src/Umbraco.Web.UI/umbraco/images/notepad.png b/src/Umbraco.Web.UI/umbraco/images/notepad.png deleted file mode 100644 index f8b8ccacc677ff71b71df8ebc40ca398fd04544b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 489 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlD;r~=Wo|ux-s?M+X=Tng(uatoqOmt^Wp#h|K(RMivrrEP!i-94CLYi z3^6g%Z-Htbd%8G=RNP9M@cj8BR_8jsN&yBA1-44Qz(Ro(vFEJZ3@icx4ULVB{$5^D zJ%Iu$3<3@gjGA_uhK7b}iGFnx7#tW3PZ)Q5c6%BDg}AyKSQwZ%R1O?b(a||`;=~Eg zOUxh*b_V?u1N%6auq!k)FgYbKhJ{)BO^|C~VB*?P$W*yzjgF3uje{)%Ba6_4)(?>` z6%AD&QxsSpZEOhHaQZYei-1Q%Z~N4#!9kPeil=BeFf_8ZuCKxr0_#rzl{VXS=xXq@GcX=e=x1YK;$aAxAr$sA;m;zVcNsig{an^LB{Ts52dm1} diff --git a/src/Umbraco.Web.UI/umbraco/images/notify.gif b/src/Umbraco.Web.UI/umbraco/images/notify.gif deleted file mode 100644 index 55671b5b4765e03aff2893c6ad2e3461d4523b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1003 zcmZ?wbhEHb6krfw_&$Z9E#K|z%J_3D-_(?`v1?#myS;V_j%IY12O-fH2?oT`~U9^zdmm~vY~6s z=I>LdeEI(4_vKU5E}fj^A9<{!z;k9t+U{LH9&HUjwr}dGQy*d@SNHY4IlVIS)w#_7 z|Nq^;vEu8OKZ_SXc>d<=_oqwm?1=pNvM)CN$kQ{apB}Xr#5ix?_CFzJ{jI~L|Noyg zwDtV|XKq=%`O*!K+w$C2t^7Q7%GcJG$N&HD{d~Xm>XolsxBTdAe}3)SkNNZ7|Npb# z)RD;-HzhuQ^YOx3-?O_r|G#dzxgmJ_!fLnB#EYv!UtB1hKKt&AQyCX8eED|2Xi|;a z{`D=bEiZom{&!(lMpez@heuLx?n*zq#N|I2FpL7qLO}5+3nK%=6b2oTF`zubz;S{> zlT*fH!-9j&tjk{cz-W6JZz@?)ZdBU3@bf*Z{T4o+4Kt@?9Q(Ia7=v5%Z3!>t_xSgLUIgw9O-~{85xu*74dKNgTx5*p1@o}VxBpp%^G~rTRrQqp3 z(IAkeYNulJK_wZNmH-YWmH-6~wy6c4k30Gs7$P~g8oqGs;&&Cg$8h7r0iHwG85tR@ E0h|Vd&;S4c diff --git a/src/Umbraco.Web.UI/umbraco/images/notifyOld.gif b/src/Umbraco.Web.UI/umbraco/images/notifyOld.gif deleted file mode 100644 index 55671b5b4765e03aff2893c6ad2e3461d4523b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1003 zcmZ?wbhEHb6krfw_&$Z9E#K|z%J_3D-_(?`v1?#myS;V_j%IY12O-fH2?oT`~U9^zdmm~vY~6s z=I>LdeEI(4_vKU5E}fj^A9<{!z;k9t+U{LH9&HUjwr}dGQy*d@SNHY4IlVIS)w#_7 z|Nq^;vEu8OKZ_SXc>d<=_oqwm?1=pNvM)CN$kQ{apB}Xr#5ix?_CFzJ{jI~L|Noyg zwDtV|XKq=%`O*!K+w$C2t^7Q7%GcJG$N&HD{d~Xm>XolsxBTdAe}3)SkNNZ7|Npb# z)RD;-HzhuQ^YOx3-?O_r|G#dzxgmJ_!fLnB#EYv!UtB1hKKt&AQyCX8eED|2Xi|;a z{`D=bEiZom{&!(lMpez@heuLx?n*zq#N|I2FpL7qLO}5+3nK%=6b2oTF`zubz;S{> zlT*fH!-9j&tjk{cz-W6JZz@?)ZdBU3@bf*Z{T4o+4Kt@?9Q(Ia7=v5%Z3!>t_xSgLUIgw9O-~{85xu*74dKNgTx5*p1@o}VxBpp%^G~rTRrQqp3 z(IAkeYNulJK_wZNmH-YWmH-6~wy6c4k30Gs7$P~g8oqGs;&&Cg$8h7r0iHwG85tR@ E0h|Vd&;S4c diff --git a/src/Umbraco.Web.UI/umbraco/images/openfoldericon.png b/src/Umbraco.Web.UI/umbraco/images/openfoldericon.png deleted file mode 100644 index 15fcd567111e13cfd9e6f1c0cd8103ca0e2d76ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFKheKdVb6i#4OEw{M26cw_Gj{yv63W{1!+$*s*N3f2 zemsk&HQ&u~&f!%4#A(caCa+!4-!jYT%bH^M9p4*PZTkKrpyj95hk2~iCLLVQKL3cY Ud_U9dR-h#ep00i_>zopr03|O{b^rhX diff --git a/src/Umbraco.Web.UI/umbraco/images/options.small.png b/src/Umbraco.Web.UI/umbraco/images/options.small.png deleted file mode 100644 index 01665deda78b4df9e17fcd2ea6fb6bc0609cbe5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 396 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLls=*-O;K0bI=Vs=nwty*Y0#^eA6DLFJ%>y+Mra{w_nGH1)7#tWFSt}gP zj0_omDlmf7G{i7WV&`UH5r}ALWYpmo13Ga6gU|zz>1=f-3<3!ZI*pu@yCpMJfj(mJ MboFyt=akR{06sR1-v9sr diff --git a/src/Umbraco.Web.UI/umbraco/images/package2.png b/src/Umbraco.Web.UI/umbraco/images/package2.png deleted file mode 100644 index 3ee17b4e3b6a86640de1f96c3befa4bae28cceda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 966 zcmV;%13CPOP)M_2H~;pm^mP1n6P(azj9CAnNxD zap&-smGy2r>o45UgWh3qTz^?|;~R?kl?gLq#(5gf5(Hg9h7&93cW<&u=XRJ1`9ygBX6=)mw>!1Vm5{jLy!cn6c_aKnG8sI z4#FY|AOIwZNrIT9JZMWjJBL0()=K9+uOh-o88i?Oi6nw-5$cS=nGaC_?T~SaJ`gm) zX0C%5FF_BsVC1O1jI%Hdr-fTy-28X*_p8FP*Ynk7Sfx2?9fvvtprK+(1Ki{=&?3sz zfJPmA4{B(g#BgOS*!?d)BgRtnI-TpILFe7s3-fPPRx0^oh7+p<4CesDr)|`?n|Qo^ zjH98ejdHbKynJP|RG#^i{rveSFez}XIhvgw;wYNRB%E7r)O|1S+t>Qt1{y~yR#QuV z<*I8x<>zL96bAo=yd1fXL(BC4R%tSi78!QNYfrzp@l`7AecjmJdR2O>E7hCJ_f6Z} o(s3jW8ucIuAjvmPdH)G802_XA+b!7j+yDRo07*qoM6N<$g0)_?82|tP diff --git a/src/Umbraco.Web.UI/umbraco/images/paste.small.png b/src/Umbraco.Web.UI/umbraco/images/paste.small.png deleted file mode 100644 index 3169d26d0fc21fc20e3bc5f8463879b83b4919bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 632 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl3sR=_y4~~CLa60w~_JPy~bIm|L0A<`0LlN|JM`#p9(8)ocjND z;s5`g|38)g|KE7@)aCyVGyi|9`VRuj(rY%h9{4}?9OnzyXI zF|u&-xvZvLm#-g*O_}`vOV9Ur`~QD#=%4ie|Bv-4g-d2l%DZ%Y!@1>V@4j#P{%&!7 zUEbPesiPAF|Np-7|J&S>nO75X=1p3B?Ay0*ap^OFHvQVS7U*#Xhms(_V6fHX6AXu{ zH@X1L`sC^27*cU7X#+bWUt(gSsKIGDg$9W?Z+k^TrmCo@_?$g6vyq8`@kqk?Wor&H zva#K=abRp_}J9QDel3b(9pmXV`OwJ?2p0$4iN?pg#``sR;*YtYfD3euKMEM zy|xYvjI26Gj~=}#x^$_irKF_fTSg`hjRmTzsz=W;F!}82`NqJ=B62}cP%tq4!Q;o1 zpQj~lVFwvAW66>wI?OJdi-ATovaqnSrb<59xpQY^1JFGzLIOfUm)ch(q=MA4RXfYE zva)9HU;tY7_t2Tchm+Yu&MT?2gWSW`A#W^nX|6uRFBj+z(yL{!^snciw{QdXs`;R3{m;V2^ z_}{N-|9|$rdN%*#yEFg4G=6^B{QrIBlk27bU+4dOU3TG6$%m&ajvt);?@9Xi`j&Fn-}Jtou|5{!|=iW+y_^8uBqbPvU>W>i~FZe?E3Tj*OkkcKD>X=z`*eD z-@m_q|9<}b`NfMD>({UU|NlQwa1@M&0NEj+_>+Z^fx(hN2V?>$PcU$tV<_f0Q@G$j zuQV$YPmD$YJ0qiQ1%t+g=7$a}Y&<*?0S(7_WyFfV7<_1KYvwm+QMr&9c%YqMOlQK6 zfCX*5ZB}VNJ|rG&WR_>sTd?Eig{CekT?LJd2?+;UIaI`|J`^}Px3CLkU0Gq!*c!sh zt&wnJ!$X&L_SmI*0SC$$nb`!7RLNR2F|vrNU(*thPFm+6$nUaa!oq{PZ2}5^o_QE5 z9!@yG&o@0|qd*&{K)+_djeti-+gq)Z<`g_+Vu=*cuaVgx(ZJR%s_nPs!@-~g2L@{Z DsHx&G diff --git a/src/Umbraco.Web.UI/umbraco/images/permission.png b/src/Umbraco.Web.UI/umbraco/images/permission.png deleted file mode 100644 index e8b99447e68a3165a8e6c0f635cd360e9df9d893..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 581 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl*Jb~|H2!-S z|L;lqm2Dws=c(@Awe8=pX+JLe{{PuKbz;|-TLrJq#;mF0{r|r5|LgpZ@6PfMza1Lia!@{Akfq}ifql2eihQEVXz#)M#Fi=oYF)%WsXH&zu<= z5p?E;ghE3Dlhv&l2SznDF;C6~(hN)-Zw$=J%Jjs<^aSn$r4{M|6Q)c`5@t4LWa44a WDOJdM&@Tf@9t@tYelF{r5}E+PVGj%d diff --git a/src/Umbraco.Web.UI/umbraco/images/protect.gif b/src/Umbraco.Web.UI/umbraco/images/protect.gif deleted file mode 100644 index 51323c0869b968fdb5d0f482a80a80cb5295e67b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1023 zcmZ?wbhEHb6krfw_|CvkVDGrFvU~;9v!r3u zg0&l$Zd^EZ+5bPk{{Q{+|Hs$=?_PYfW%y#l@ZOr?xh2CRbA|`z40p{K?wB%MGiA7J z!f?@;;k+@!Ib(*?Mhu4ynGP8+{dZ=)V#)T!ne(m<=YJ2LUv50_oOvEPaR2w@zi7+* z-&^3nkKheQ{=Js`{{zIndyAd06aF70{lG)ws*CvlD3$-ws{dm(enhH2jnw|1YIxK~ z?R$#pnIN72*;fB^?Ovr?|1WU-S>*P=!uNl5;GHbD|Fywy$^!m3g#T}fd{Ys8uh9QZ zUG%l0p#Po8|GQHDcc=dE&;CEP=>PQ6D>aG#XI6YYwDJGxUH{MR`*~{jzY7O{Up)N( z#+m6J*ES>W<2FLe%*2&bh_y5Ji|F56^|N3cpgzK@ItWyoSE%F?Tf*fuvoppP~+{=q+ z{{Qyn-qy8ux2(Cic6q6>R=JtZ)R>SH(Kti4#KI}85prO|!^7=1 ztSm|e%8#2`xg||X3UrnUHaGTN+VkMT!RE*ONj$0=8V=_rjTD@EP9FAPJi^K-!s*5G Oz=65r05=;8gEau!MM^^e diff --git a/src/Umbraco.Web.UI/umbraco/images/protect.png b/src/Umbraco.Web.UI/umbraco/images/protect.png deleted file mode 100644 index a71c88b709a659dd2733a93ca914051d5b2c2347..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 628 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl$6WS6iq}Ic z=BsVFcek(qKRJJEtlPDo(tl|tZ)>9(+@1fwxPL&E;Z3p6|2VC?5!!zn!Y@@PHmV5U z+O(>ntmMS>-dC;ypG_Ek7&H8BhZ(=H2sjaSW-rl~j<{!kjL^z@fmF?#5=2ko-7-jgx^zAf}CButbIzS(U}O=0#_!8m6a5*aVTtH5aymd z%TRju! z&~{N(RZOLlp|~UBFH2EKk{baj0QoH-`6N=F<7HaL=VVais zs1I0K97pGuGccUf;Oa|A^}k0fzVFD?>9Yf5Tl$xS?uH3BBS85?W218&`NBNX&seaV z94g&aIE)rjeQPN3I^@VZSeCQUb_Ccj)6_uG9Evi7Opb#}8!(%Lu$nF?#IfR9gj4&D zj$RQBUDw(KJRV`8LWR-ixdS#ihFVTUR*GS%;}vL)hS92GEH($XCSd>3!JQlk9`6f@ zPxx0ygE-l|iB_hLPQi&KGln6i+UBKqPrm74<`7S}3B`9Cp zLODWEju4a|5R_H&AEp<1=XD?p8{rLIIuV247JGoX~`O$yu zkGpqm*O^5!Q#B*g<{f$X|KW%Kd6O?j#r7~ZGX4hxi?ge}fOaXA1o;I+L~xP}F)`9_ zfoeZ^x;TbZ+)A48d~zEjXQcuIhXPxrou#FtBO_y7T^Xm_0tN>LM%IdoCk+iVXU00s z3XhXhXlP(+vPsw{vb%*PK-hu7@PskfsgoN|oa#F1wP_8w>&%Ko+c7 zqabY2Dk&k+z{J4FB9f3aX{xBuW058f1{Q(lJuDs!97iNwTwI<$Wdzzbk6T0Gn9?~V zCPA?KCCkb@6Bjh{2rxDFk4u ztCzFlqpGjt@&9OThvoDCtj6i8sjbM~|HR(^MS9`E-2b`H?QwgfveW<3=l*Y+>d@x= zw#@Rp)Arcx|9yqAw9D&Fa=nA3=BTT&+S%E++W)lH|CGGgu&}UhrPq(C+@rtJvc%Pg zkh7@H{l(z^q{Q-|syo803m72QL=k=GH zyQ077R)*oGsjX|H*kzC6Re{^E(fz5!=&Z1|#O433uCLSP|DB=0o~5ifh{!F0!`Ru} zma*lKmbb0G%Biig+}`0xa>$vTypx%^P?65v^8eB6|D&d;prxyup{s?9t%sYfvB~s# zf~LCJ|Mvg?g`v=NpYe%}vEALZL2hLlI1n;zGhWkz z40aSiWOg7o01DrDF?KLNMs_M`C2Z?VFLwGiL>2`G>$qgm_Xf&>4Ftlh>!L(L1u-Uy axWI5>-Hs(zeDJtJCyEXS>mV5l1OPi<7))gV diff --git a/src/Umbraco.Web.UI/umbraco/images/rollback.png b/src/Umbraco.Web.UI/umbraco/images/rollback.png deleted file mode 100644 index 0f76f58a604db8e10e97aea36561ebfe01d58a3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 626 zcmV-&0*(ENP)uS`bQGzxa!HZ8d+f)x$&a>7SAP8gD zRBC<>U~VYZnekHFrRVQ!TH~V|xjr)hdPzrSGy(EAiA*B87`I+-RxN7XTU`=DghrbX ze1M}SAVNojVj>iMyjE!WBEDaG^Xm0V?pY#n_Ov8`K+{eYW=&uVl}fYHP*#{Si_vD- znx5h8jxumTu`tvReb>XcTh75j)dct2^5v_m78+c?A$q+U0f^Kgc=wfs+S=XU zc$h!_<8OX;uHQO`s?&GKh#Ywkk~8n_Ikn$@EE4k-}hasBw6{gA62u27`kq zcWqQuF#_wzWt5QBi5hA;sLBF=l_(UnUv_QHSMBWJ5f8}}nZf(rQP2Y?(pmO!ca zU<7!5r|7LMN0P}|Ug(ZX7Z^9K0`QvPwxEsB)*_Q%2ox3%!7`!M){`C(v0koy`S39a z0_+KPHT!`y{gZOkii`o-Y_`|6hWuwRPp1a5xAQ_2Y}+=B&|d)t0CJ)U)b~)=+5i9m M07*qoM6N<$f)>gckN^Mx diff --git a/src/Umbraco.Web.UI/umbraco/images/save.png b/src/Umbraco.Web.UI/umbraco/images/save.png deleted file mode 100644 index 169699cc8fd529d0a2724fa9496c50722d11ae45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 549 zcmV+=0^0qFP)&^DpB>u_D=?a_}8qW|$eC_j#UoCUDL%ON7iA(VxZ37t-~5uzuO2 zh7N1S7~^MaG1l?%Q^MQV`z%I=$4@qcDS#jdIXKX)uEs=BFjeVmYsk*-78lN)Hn2I( z!1^ju3Y1*%0CR^IN@=ie%zl!xcJV65Xje;^&=o>3c8$YBp$vmu`8`9wD3M;44NSF( zlWt^L&eGD~>TWUuOXw99M$^j%C{tE5C<@KS#tIJ~?IDa}Amr~XeM*VUEN2^a%Dkus z7>z8==5gZMiZ?R_tq6u0H4)O#^Jov^g?bs{CpP`7LFhD*I|88HZap0iyEkj1N4#)_omT~0&)#?|1ZiiUMYs2> noz9H@>0|KUKhw+rKLr>7DZuk9I~en^00000NkvXXu0mjfZHV`) diff --git a/src/Umbraco.Web.UI/umbraco/images/sendToTranslate.png b/src/Umbraco.Web.UI/umbraco/images/sendToTranslate.png deleted file mode 100644 index 77a0daf79ca5fdeb51311f8a965ae14305b53f41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1047 zcmd5){ZCtE7`@wyPBL^e#5jXVMW>r?q=q>^v%MW3wu;>yAQTMdEw+@ojoy}atsm37 zriHdTxU2(jX&kT8HfslXk;p~@&D}cKoy_uv2tm-~Rhhvb(3>rq`GeN$f8mpy=j7x` zo}8SVAZ>WLApfC!ilPeiy7Fp;xBZ9Bo0Pp}?4qQQ%Bp=$OHp4)&*p`GRrGdeh0$4K zYjL_w4hy9<+nOzE{c)4kQf)Dr-?3k}yhKqOZqZe>6|yXoH5qb*VLACuj)pAsaH5FO zc|pWP0oJKSS|dES6*80XBZ@Oo*UO7I_qixhywzR2$!5#05p?IFFb$v2woz=a~IK?kmK3?^>*JtqOJeu zFMrRBWR?fKU9LgS(9-3M_aOmm8yq+LK6d%zgk{9Jn}Aom!qLm~@)8~xTVN3LO7J}_ zW60y?_A|7H(I9B|4Fe-XLq@|PjB%)cLZCstE1>4ViPQ0`->y44Q^BjoMIc%u7)JvuvK69rY#%k=Z4P1d;6< z$#JXYbN)){iD_zR?^DZJ{WKm8;0`#1$T8I+AB&8GJ_$hA)T`A8>K!AKxtzxUq-z|ISnZ@-wYo|DH0gv-xdR>Ek7(pENFPtmyw`Ho15RrvBV=|Gw}~C92Twr-w$PgY(BO zKQpL{tf*p7?d`c<#+7+bP0uBqB}+wr{MPk>!9F!P{ppSk!#~26zb)I1Pv*V*z-qm4 zDB62(p}jR=g3RhRKD%;d`tjI(g^?RlNv7eNlzBsZ`t#1*8LOEZ~bsjQ)<`Osc(i%5$g7?&1&d}iE=`w`if0V>PVyvE^wF6nqJdENgmHSY1wgPnS0Z_xPzu}Ur*?O4Od5pgJ z`u!~2oBp)Cm0}C%XHWM&39(q3^0V5k{1_K5$ i6D>*s2`5$t5eHcg2UXM$Swh$v5h~q778xnyK>#}m0-lZl diff --git a/src/Umbraco.Web.UI/umbraco/images/sort.small.png b/src/Umbraco.Web.UI/umbraco/images/sort.small.png deleted file mode 100644 index 8845651fb340417bed0367767d131d8032073abd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 400 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlPD z`O}}P2Q1Z#SX(&jg}VPD->S>5c?XjEp0zA{RX*p1UB+&!Zai|_0F1d~y8Q RCZKB>JYD@<);T3K0RRfUmskJ* diff --git a/src/Umbraco.Web.UI/umbraco/images/speechBubble/error.gif b/src/Umbraco.Web.UI/umbraco/images/speechBubble/error.gif deleted file mode 100644 index 314b1718c2b4065e53f24455387413b58bac31a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 582 zcmZ?wbhEHb6krfwc*ejG8R_}_Im7?|On?7Ah>rIB|DXBA3x=;>FEB6w1z8_IKJ))S z=l}oQ@82`<^K*Xv%JB9rLqr5`T^;M1HChb~4Bx(Sty`!1@FD-xr+i0`3dY67MnnW8 zB*g6B@3ecj)alc^dwOjC|L3WxslR@m|HB97A3qpw+~{|AXIQmr>FLvg-@Y+ixUg^K z%Cet7c^euUPo3Iz>(-qA|M|1CS$FK15)ckY}pGLrxI z@8Qv-HkFkO<>e+vj!5p`Z+HB-_@__TfBd-f=g*xNFZd20E=Wr9sjABQ@k8j|y_M$PgaxQB~Ct7RD15<@WzSFjyV}1BY5b@h1z|C>;<9iW3I*#SLjq z%`L5MX$DTlHtj9!eyoo2%)L$STB#CJTvNj>cvN&1VqALFQpNauqEhsGZBy+y0+@u8 zoZGykQ$;w`Q@s+kTjR}AQ#BKeSeZg&r!)tt9yg2#F;7*Dvub9SJf6zw;Kp=ZPUC_v z{{vC$WX|K_GL}vL4^jgKg`TEz+nY97nHVIfHZXETGHJ*(y0k1%(wPKI5Dg5BT?}SB MW(2JaRA8_M004#L2ZZ>EP)@aT_#nLAhKKmY!H|7FKqj`vrnUV~thA@$l7Np7mK6Nd zWw!nz3nDy81UQFhJ44;+)U#dp?tP+p_f9p^-VP}iB`F5aFi@MHL-E})J~evjPq{sv zC;X85vSZfad{{Z@VL=4%8;Ftt-}9nrSv=PF=)uEX2OrVoY#T``bjmk&U_$`Jz!4wr zQW3Lnz2%>Ob@Ps37Dq;blRAY@vVICk-!qwe@}7f_4=qhiBOVRG zS}Z`3RSZ0I;J)cUp84tDO#5(S5QRWiOeXByes}kC;ZOvV(=(7FVJy0P()V4IlwU)4QctRB4mS#dqQ&($dP+v1~qg+NJ)d>Ag0o6nTZHfe* z%fnD*L|dEo3zqr&RSw?D?gShW+-x`m6ri;3+x+WnmOYIQYHo=j_HUfvZNFb?rE~<40ImbKuPs{nm8#8lX zJ>xmms-_X}JS=-QR!c>A6s@a;@E3vMRBrZ1f33UUk8(C#@I90$Wy@sTqnacnsZ*#j zmM9W_@^rXF_0Y*k$@lX?G1oZz?c`GF+cQ)sm(pZWE5hS&;}ML8RGcd;{oLaCZXg?D zY_TrMFeM2F>8X)UnXx9x=FIn~uomrE$3crjp}&VaJfW~KdG3z8S=T{`QCCsP#INSrwZG3kr0HQ0ab~a3@=3) zp0-GNCGd(oERc*qosuTGE&(5F5ZLQaDMH$k#H^w!FbL@iX(tS$;`3#R-tXv+tKS_` z<0+#?T;yYSZ|~z3#C>^l0r1;qDW_382hLj3ca$dD6uR7 zt>Ug?Q-qk6YEbt4+k#E`E*i#_D23NaTcsTL?ccNIi=TM-P}UO=A;l?CLZioU(X@~J z%kjR_;KFXANTS41o5*OZ;~eN{$?QpzgBc>iW@?H%mOVj<8JG4|D*8K##OX!X>E9j; zVM2GY`1UY7Vsvw?V}Okhs2>6vwePmNk0jN<@k7&2(Y6S+bHtoijGdA zjb%#Elxe*=WEeSVmWoRR_Tu$o>A7v~+3sG#LTSOKLE)phtsUKc58@xgum9%jX5q+( zsQ%Z#WI|I))sEWTS+*-2f-yOT&9b;ogl9{7rQrF$3Ytf43$dK#Z9wb5qFDK{OV@Yt?fgvFS}8>zr!bZXnZdLTE(gn|Cn2-5k+Ek9rjPbU!aI7RkwlV;DbiV$+Htm` z8)HW8Ceh2;MaGVk*W>f}?{y&!rSgbL{`Q|y!yTb%{ zk`<|Qmuo;S%GhL=?=P-9NBeo&9qZp6AFTffFaSr;e9@{93g`d;002ovPDHLkV1nrR BnMnWu diff --git a/src/Umbraco.Web.UI/umbraco/images/speechBubble/info.png b/src/Umbraco.Web.UI/umbraco/images/speechBubble/info.png deleted file mode 100644 index e5734da6ed44bfe2c6efbf91adfdd6f65d36848f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1542 zcmV+h2Ko7kP)Za;TF0teSz?ysoa(zt5b+U3jZ>Z_|u9X_8-_`v9B z29Zd@xO}OjqxGjN=Z0gEBf9lXz;cwSX<$xECAC0?z%D(K?HOY z=;}%1?AhNEKb|3dxi?9t4q4pw=)@TvUR_HLG{JxsB(t*4H;}g2IZ* z&7r2o@>pk#HX7gs9z35nMxyB!uC$w|EcM$2ZILW)-5#`Kw}HpaG>LGCM@9YIqHxu$ zSIPAq&V}_-!@9Me@QC>3+6Xdg&W?U6IJ2B# z!0q=T`qz!F&I{+8L{V_pI0I!3x4NRpCWjzN(me#sSfw4*I={B2>!zMBfpEUTJ$ck5zoT0O@Gz1A^=ExUe3 z&a~`9o`^n|v&|&Z!e9k2`Mq;Qu|N(esRTxQ+rUdc7^aU1Oft%X9X6Sw4hM< zplBAAWpd&)%|s@v*rg(Pm@IN4WSwOwNf?=aYFQPQC_*%HM!(?n*`VrZKlHH(^bAcB zPi-lY_K38qRHn?z2?7R|=;?;0FrCw385xoinoNC^*ppxqz+*8h)g4jO)ONusr(OPP zS4%yZD~>coa8X<(a25u0Lwau~ zuj%SdL(N<-3^mc%$%N@5CprIYuipI1iBlAe+E;MXJ5Syw{OFl?!#lH{%7$)T&9?Ju z|KVJsyW@0m%|bZ+vzUC@PPQ=+-ZM@-g@8>o1LfuXdtuz4&X98!VMgszj1BaCo4s== zZO?wPxA#jr#^}riPv=X-Ir5uWL?VmSK^-Mb!n5uYN%chMB)DBx2WHePgF`MzjP%Bj z;I6nRU7rj_uU>8`Yp6%boRxV3NK^(#e$c2Snr==Iu$Hv9vPow|4BQf_G<7Ijvf!#j_BV~IAL-LxnMG?pZw@V5vD+&Z&-PG+1vJ@y zS*A>6It!jASiwjtjS(8O5xP5*TJmG`l$zte9Ws z^LYY3Hw2P~#F&bBawHq+iHGB{?$blD*jL66Z^!@Fe-#rBYYvE<*F9U3N5e28hc suBd%_I^AuhGF{g9Z|T$R|3`oU0GMi>$mrPZ4*&oF07*qoM6N<$f`J9>1^@s6 diff --git a/src/Umbraco.Web.UI/umbraco/images/speechBubble/save.gif b/src/Umbraco.Web.UI/umbraco/images/speechBubble/save.gif deleted file mode 100644 index 89d75c110e21c4b20ad72e60ca5832c4a149a60d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 562 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 diff --git a/src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubbleShadow.png b/src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubbleShadow.png deleted file mode 100644 index 40c875982fb5b8047f57abaef758244f714a2ca1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 916 zcmeAS@N?(olHy`uVBq!ia0vp^?}0dmgAGWw={-sUQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS=07?{mGT^vIyZoRqFo43e8q#^L%uYde8sb+Zz->uFq2y0`IfE&Z#Hwv}f$! zlzjhKq5hhxJ=XdYA8+uvX;@&LqFAPQZ{MaAwrdx~+A?{z&tFzpp8M(B%_E|EInqVu zt;d713P&Jj{K)i(6@-s)gK&qqfIbK-Ko!CiH6bhP5Jy%BG3~(PkIPQk zo$Tj~TXrio{HA56*gd^RoAq{kJ!NY9&TBq(+S%o~Jkea!4}81WH&riK_g?Z1H^#%K z59xmWvHhyun>%f%%VwR*-C^4=W9Rw%h(>;--0=eb#M#PzhYvlmZOQ*pQf+_t`Y+p0 zjtjT7zV3IaE_@TkQFU**$h-X~twoNN&5y6O7CbKcDe|sd z?!$%;EbT9U$*imRxuxQH#_kW_@BdYJ5MtS;%X{Y5&gIUN=T=K;Ev`MY&(7{c@8P(| zbN@eUU)ika^LRds&Gs*MW9BtznOdYBobUVk;i+3SbrUQKTTd@PYAqJ^GxNZ4rvqD; z7XJ$h+-y?m{dN1pPm=8KHi#GRSvuM0JDW^%kN3mmryD;%-(%rzd*??>?&GJ5cG<6v zg+9M@dhvI*nCiFNb{cuiu714t7sChcr2;8YGxmIEnX^u(`@Pu@+k;mh9Fv}YW44@M zmw1`fhKj`HjNK5%--gK*iSrp}?@>tjcxW1sy)k`ZL_A=R(n$;h8_+t&HPF#fjgDZdj`~125PkMDl;@^hH_FFpUt5j(`uxH|7 Zn6%tE@m!0aH84joc)I$ztaD0e0swxVyrlpD diff --git a/src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubbleShadowNew.gif b/src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubbleShadowNew.gif deleted file mode 100644 index d2e68a15e7cd74573ccd08b4ee46b9d45fb5ff99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1774 zcmVe5~Be{CpdSIx(l{XwA6zn0?*I{kr>JHy7|2`j2 ze{Y{(zmLza-w*JgK!5e#!Kz?roWgPo&oO-HFk&}};2KW62##BWRsV2sxiJXRPRNfU zOOiaf3gs}B2UEIS`LZRtNsxv{R4G#Fh-xx*)Tq#+Lz^;vdQ{>9 zgJD|kylN6>R-IdIcE!pPtk$q!%aR0&whtb9>eL=er}5FJic!tIwHw!>6}nTZQQ_OD zQrn(Z2^w?f%P?ZahF=W?=$K#RfW+!nF(^dNc1x>v zoi_IC!D~ZTMy*UEG+@Fr~722V#y%k`Do95VNnUqS!=xUeF87Y~@MmmnBy#I=s*-At%mSv|2>38X3^%aWk zvnz7wtWc26Mv1GM$(U@g$^tnhnxxd(=OYz}lPWlhHa^Z#eN5yDNhc6D!`sG`0)zuMt~{aj$|INb$3O77TK;Ia#M_n8*#8 zEy>U_#@lQM>yhif5X%cE#?ZM7p_$jId-JEB${BRdNane4mq`mdD^CKK6|hgRMyc(b zC(rA0)#tT!F0nbgNgb%+Z5J%OK4x6q#b$Ty^VYh$JhIFyvKSag0GdVcl8o%x#Ek^lGZ{v2PL?X%iN2Q8kAfB#pVuc#oG>mc|uPxWMM}D}? zxU&s;rm;^i{p{Goo;#{CXFi&tEidmG_)F@_H}j}Ay)F4!L%?(Vl>huZ?$v9rX6vk# zyZyXd;0m|*St2DD(U|&9^_t|pENhAK2+SC`yqb9oS+v97uqt>zj^S*6(y|~1&GUs0 zatwL-fMD=?hq@1G&VAog8sJQ*wG%Xqh6V40F@oX|2WP%6;fsmo zbD?q&$i~Y7F^iK6+ZVByLzdkUfi%Qob;7tmxe;iA_hMljohUL@nURGTOp+m+S2!(R zP<|TRV-;Cv5g%64k47t8AcWYy#8K^xQCuW!QbtM-IWm>tYM34`Ikfz7QkGb9oF6AR z%drh5JHYE3(g?`PBpz@>!g*u>3mHsY!Ow&8L!2c!_`y{^lR&xjr6-5DGr8=mQBgdk z!4eok`E(O=dP*kAVw63z;c}XLB$~yFNXvKCkew3L9SW)0yflvUmEcsQ1I@WAV+!<# zbIhMQbx2PuKFoj#%}7JRXrL8Fi^Vd9WGFzTLptw;O`ip`)gjsV6AyY3Xw@v~#^xzc zI^Hs*b~+kCA^I^RLYy!-JCXnFjj5sr$~>7ejYlAy-_ zUJ6wr%=D=>1tB0inbfRm^=PGn=~lmrRIGw^tV!KwEy;@3vOX@YY*j0k>e<$~?tpu8 zt!od=icY)gH3wbXt6%5Z%fAZt2HhE~VQH{0xE^+~Cr~V7ADaSQLUyv0tt?veYT3+c QcC$ioEN4H9f&u^lJKo`bDgXcg diff --git a/src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubble_shadow.gif b/src/Umbraco.Web.UI/umbraco/images/speechBubble/speechbubble_shadow.gif deleted file mode 100644 index 2f49604df553b82bb923f713548b3db81b5d90cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 660 zcmV;F0&D$8Nk%w1Ve0@`0HOr|&d$!ax3{FEq~+!1larJG|Nj6000000A^8LW000F5 zEC2ui0P6r)000C3Si0Q)Fv>}*y*TU5yBuIBj$~<`XsWJk>%MR-r&I{sc&_h!@BhG{ za7Zi~kH{o&5CP!?(5Q4uty-_xtai)odcWYXI16|<0a1c_&2GEj@VI=PJ@e)l9QB`mIMKgf|s0~o}ZioOa+0Epr@#*s&WFEq-d+L zva_A7Or^BBy1Rt7uL8Wm!ozO9xWvfGw8pT?&d;FCz|hpylhW4N+J@NM-rsiJ;N#?M z;^pYy=jrU$>+SH)@A34*^Y!?;_xb#>`~3h<{R>DiRDfH}3M!0t@YlkJo(4jk$gSZq ziWnD3%*gTo&c==)Sy@aNvLv35CQ}kXsj?)?mLOlk`JQCr^bwe-aET z6d=)}{Ei~Ur*tX3rcU8OjmmB+RdiOblFOzcC5RR)67yVs}>sDw(`)z z)hTyw&$@PjHY`9guTfv5()u+TU@zan0ihfmr9d#^r>zVxaQrx3l)@k$1Q^4)v**vC zLv!YnrSRe=rcM2#81g9{%{y!Z?~worIj&b+zv z=g^}U$OBlq_3PNPYu|8E^7ilG!;4?;(fj!G=+mPQXI{Pg_wb+BsUE++{reZ_(8tfe ze}6w&?fEC*fa!_E7=Z{TC|(@1?&u?aIsPbQeFsjE002ANH({Fq diff --git a/src/Umbraco.Web.UI/umbraco/images/speechBubble/success.png b/src/Umbraco.Web.UI/umbraco/images/speechBubble/success.png deleted file mode 100644 index 90b70efcd109b084d4239071cf8361509b5d6b63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1036 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 diff --git a/src/Umbraco.Web.UI/umbraco/images/thumbnails/member.png b/src/Umbraco.Web.UI/umbraco/images/thumbnails/member.png deleted file mode 100644 index f6a7bc2249058d145166ca358c668a8dd79c7035..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15119 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 BTe<)M diff --git a/src/Umbraco.Web.UI/umbraco/images/thumbnails/template.png b/src/Umbraco.Web.UI/umbraco/images/thumbnails/template.png deleted file mode 100644 index 1f31ccd07bb7b63c28ce1c909545663b8b99d8bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11883 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�gmQv-%%6Ns8HF_!kp9%_P@?=5WHE1 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 F0sv?9ij)8V diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/developerCacheItem.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/developerCacheItem.gif deleted file mode 100644 index 8a441c6261c64582ddae262ffb2d1b0661073628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 577 zcmZ?wbhEHb6krfwc$UWS_wS$Y-@ZL~{^t3kN8i7H@7&P-|NpdCwI#jF z45Rda{r>g$@4w8}#ZR6*`TO^8&79IdfBw9C`EpTVX>hLhyN_=_e*SX(-qZJQ-+ul6 z^V;FV5AWRh^ZQp!gWuOLU+z44SU+Rkf;IDh|M}Iv`oNt%J3oK@w)@zTr;nff`SZ6a zJ#%Gl_M>MHCV6;Fjf(#H<42aW?T;VdU%r0+`P;W2KYm<4er$4L;)$I*eR7@JmTZ6b z@$<7+Po6w@U=plbwYZ>dN!y3d@5(1EUASQR&!4{+tXXg$=#j^dGP_pnKDN^_$)-0Z zwZYBx|G$5=Rt_sm3a=eMo-jG|>5C^1uUx%x`C@6$oXsb%_7&%S{`BeF&tEl5cRzXc z{^E_t%l4jGy65zXD|bIVd)m5Svrm=%oik^@eEq(0+Z^fgz1S2V^8D zP8itdHKe69x3spVHKlfS_jILnq@?n4`1CULO-R)=w2ESKWm1c9PVMg$kd2FpwM>wZ z5??x#fh{yr#houYHF;BKysDFqC(&Sf>M~;q~Rgy{l{{5K4 z+IyLF{{H^^^z?qL%(!4;s$5;vmzJ(tTGo`6;_dWvoVFJKWf4_)`hQWB6#*c%;gS3N#ca(3S zM@PPxnP+85d8Nhi_3(D1!^d}c#Mb1xdwZtH-j1iXS&v!h)6;f{rq80I;JUh%xzUrk z*!=kT`~CgZ+~t`th|yN*_oEZyv4Q0v0`OXz|g(L#=4ThgnzemZ;_+Z=*&oYK%cmqonBs* zzS#Bn^pZzQoV(Ng{{G*_#eBYl%-hLov0i79y19dc!rQv~`}&Zpp;v`SYMWi!t*!d^ z_npGq_V)Gi=jUOgSAwI%bg5>Ww8{DK@b2~b^!WSPrlz)9U;qFAA^8LW004ggEC2ui z01yBW000M{fPaF6goTEOh-G{fNltq}h<{H7Lm?nhG?7wdb2dUSd67Xq4n<)V9EU%8 zRWc$9K591!g;P67E(bby7F=#@T>^u3Yco_(BN%5UDO-3MgC#2x6Mjk^DrqQoaA<=A zU@Z|3V^=r`S_W@eg9J4jClE$2UQI_#7gmD}IXr!FX0m)?;bDOW5`!4XA+jVu06Jm- Qd;k!mO{9w$2?YWGJKB;x82|tP diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/developerDatatype.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/developerDatatype.gif deleted file mode 100644 index fd922b92dec8dede7b09c1a62c415c4533cebd6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125 zcmZ?wbhEHb6krfwSj5Kg?c2Az?>=q1@qmGW;s5{t$;rt;63ABk$->CMz{a2hWCK+S zFfcGXW$d~WW#E;`we!a<9^;o`3S6@3k|6Ss(@6Ihd-#z+epkmrX6@ diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/developerMacro.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/developerMacro.gif deleted file mode 100644 index e66d10972c34803f6b3833dc26b62b50652f9e57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 604 zcmZ?wbhEHb6krfwcoxR+<;$0E-@d(i_3HWa=UcaKy?OKIv17;b^Yj1w`SbVh-&?nC zy?y)k(W6HX9z1yQ;>F{~kMG~VpOcf**x0ye(W1h_!V@P>ehcdUWmDwJj|zJv}{Jwrr`bt$p(3$@%l=`}+D0A3pr>;lqm;FHV^< zB`q!O&Ye3KE?jv1`t_A7SC%YUGGW4mjT<)}KYsk)y?dKCZ*FaEO-)U`diCnvyLb2P z+qY-Wo+C$&)YsP^Ja};a{P{nB{)~%@OGrpKaNxkgg$w`v`}hC(B{r&882}Tt62ZqTuPCQclJ}RMn!UA2s zcCsNFLfYP;Qf{5hO8k6+%|VP7k`m40a-M9W&8{A-68g=|>@ESS&C)8i?3|s30t#G; z=1N>#Y=P=J&5Vv=)47#pSQJf}7>%0c&3K*J{PYZ1E!kHwC^VdNX<-u6kxAUJ_z)Yz vWg*=e28Xy>`I*HpoG{3oz#z;dVKU*t!p95bjiUkt5{v@3<%BUYGFSruU~usC diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/developerRegistry.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/developerRegistry.gif deleted file mode 100644 index 40d496f7ca0a1e416b18d7448ec52cb9a4df32b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmZ?wbhEHb6krfwSi}GV|Ns9tPMi7k!>{jOkN^7l@c&GZh~iHcMg|6U1|5(JkXi<2 zYmHrZtPH$erWgmmP?XLK8h|hD0()1o$KqpIgw3DtB>#4wcRM{jN7ZZeVR>0 SCsVTKwXZ1gvQ=SVum%9Q{4^o} diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/developerRegistryItem.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/developerRegistryItem.gif deleted file mode 100644 index 0b19ec1607fd58d9b78ccbb0672c3a0aa92ce15f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmZ?wbhEHb6krfwSi}GV|Ns9tPMi7t>+xSdAO4>S;sON~f3h$#Ft9P`fK-6gGB7)2 z?7E|7;B|s!rv!iC$sf+!N)vt-c89KM?a+D^+Hmi;fUaM^=DAH<{qiK<2o}1|cz^wo VrtADChf_BBt}|;do5;Xm4FF+%GF$)v diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/developerXslt.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/developerXslt.gif deleted file mode 100644 index 7204e1b54fc01e2a310c26068f8ecace98b45acb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410 zcmZ?wbhEHb6krfwxT?wU^5x5S@7{g+^5xB&H{IRct5&Ug{rdHb7cbVXUHkFl$6vpG zeg6FU)vH&}pFdAbOsuM^`u6SHpFe+|J$v@`>(|w*SFc&K=E;*MpFVwh`t<3>jT<*@ z+T`Kk@!`XVA3uKl{Q2|k+qduEzu&N7!|&g}*R5Ok_wV2D-@mV4zy9C9fB(UNfnuQe zlZBCiL6boTWD>|v3~bvEgq(8GndNk%$4%L%H{ikwk)tAgescX8(w1{wR`>`zYR$Bk zZS8o%YpOl*sFOm3k3!TW3)fX077C4w3Y@~SN_m>FH&jtj%ls5DZQ<=^+JZ;^`TE~{Rxh(wwmI=&8^m_ z2Go~$U#nSP3}+fA*AZaIOc3!n26x>B-W?G9%KnF*f8g1(r}tuyrv07*Qa}U(+3j|R z!yyPlHk&P#O2uNaTrP7QSE*F0)hfd`rw z!G!=CP9&0KIydxsC6QR6>7O*kd|Cv8P~p63e{pnn6fK*;Y8Qxt{cb13=^ z!y;eP-~|dMe}GrZe?xM~rL@=Ubvm6MkH_tHyId~t59oiQDoR8Ugg}SZBx_F~>9_$I zc=ULn|B1e@4>8__P3BQ}cm%GMY~Fr&m-<)_tT!SeSX_HVaqvLrrPD7hw##C1$JV+Q zU6adbleX_^+_0_g&W?`=1UKy7dFtlXjS>yqZ~4-7;>N8j%CpCN!*lDK&u=<5_W%F@ diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/doc3.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/doc3.gif deleted file mode 100644 index ec577f78b5cf98b372ef73ce4054082e3557a0bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 616 zcmZ?wbhEHb6krfwc$UYocJ11)U%&qR`Sbbn=MNt~eEIU_-@kwV{rmUv(=eNKYsjp{rdIWw{O3E`Qn|_{pQV^BPUOuJa+uno$Gsd?|%O5+2?QHFI+gk^Wo1A zU%nqcdScsyA2pMsOz8`t9_|6EB`W z|NQy$-MjauF5LF?+4JX5pFMft7 zX3e^F>o#oIuyNzY_3PLF2LlGuf#OdVMh1pF1|5)AP@FKZA8p9XX>Q5MYR^tj&+Ey| z?Vm6)x4So&i9aq-m6?^#IXAl}k0IA5*hO0-G@RRb!J;Km%)A_YoYsuI>wA_s3JGh5 z38pe~?9R)`Wld&`h>>y^kIl_mbkamw-dsjK$V~FeHFg#ce@ib}HD9BLJsE6n4q7?} zX#%c_Z%*lpUt8tYQ4U*TLd(dL^v3%0X+3ir2qf` diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/doc4.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/doc4.gif deleted file mode 100644 index d9dbaecfd70755c9172fe072148bfc3b0303aee4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 597 zcmZ?wbhEHb6krfwcoxU7cJ1046|xvN(nKYr}(>~iMPg(=gzo;`cEd(WN?t2QiJ zv}DTmqdWHOELiqx|DJto*RC&G`EK^)uEK?ncW>Ku@!W~L^rE9DPhY)!`O>9J%}dTK zUA44-^KGxhjKlj59X@#Q(W6JX@ri5Kt=YPD$Fytz&s{jTeCe{ohYl@Yx?s(^br&w4 zKYaAq)yr4cuUoh0z=8AU&)>fP@bKZ|SFc_@fBO9C(`O2ozUV*sx2Lyt$JXr^&Yin> zqQY&TrkY{rJga-@bjTTJe0#h7D)WoZ7T$)0#Cv z8`f>uuwmoIjqBI1{|^QXvUi!_!&R#M_$BO8@F9&LH0QV*q6aWAK diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/doc5.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/doc5.gif deleted file mode 100644 index aa4c644d632304e4c5c6b726aba0fbf9c91a0d2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 609 zcmZ?wbhEHb6krfwc;>~hcJ11)U%&qP^=rwJB|m=r`1bAF@87@QzJ0rN>C&G+f4+bJ z{=h!6vIT`Npi2s_Vlvt8j5g%7RJ9+l>=?VMH zn|!2gy&2f57|h!Q8q(6vpFe+KllI4tAOEitol;pjXXebC>O$`s7^b6p5|36~*{Q2{;JpHFnpFUw`dGX@KEiK(EmoL43{d!BP=iEe{Tio2AK7BG= zD^cpIG1I}}(W6J3Hf>t7X3h8S-`A~Mw|Md5jT<+vU%!6Ch7AV}9N52q|9>!G7~DYd zCkrD3gBODi$eExxVPN0a;N{xf;_BA!VsG!&tVWMYuuP5_#j+y-Y+T1QZUJRbj zyzct$hVFLm?DKmTIkS3Ld9bf>XI!^%v7-m02bcRn$6XGK_bYiYxi4jOTX4*o<&=ts zpsBm#`5p%z=e;_XCgxHSoVUFk`0jhys*4y{YiYi4VB6>+V5FufCo6W8<-u=*#FlO@ z8I>1I6A})XNQ;|2crfwc0VgJA2Zfm%4|sHnF?KW-g)}%YF*5O3Bz)+YB&2ANDCD%H JBT#|C8UTvG(+mIr diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/docPic.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/docPic.gif deleted file mode 100644 index 3985e9d78677e953101b03cbe2269029c7dec042..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 607 zcmZ?wbhEHb6krfwc$UkscJ10@88d#KWEK( z__Uzg7=g+66hxq$@WY2ljef;0kr%#WbJbB^5`RKTmtl9VX?Amkx{{6!T5ANT7 zz}4I_*U4|{mH*G4J=?Nj12?Bc=A0)x_wMameca0}`0C}$j~_oiec^2OyvJuxom#YL zNq%Wl%i%vw4fzGjU*{G?Ihz|MBzdp8_cz1U+t=UK&D>?}x;4eC-j=U>w{`1|Sy%sO z&axc6=)+j9P^>?leh0b zTyo=K*{=V^Yd-GTdtm*#b?2^LJ$~}ow{PD{*M2&G`uy^x%QkJ=v}Vnkb?eq`*sx*a z#*OROum2AQ3}gYtpDc_F47m(CAl;xiVPN0Wkel7ylAYO}m6n#H25SH()F(dx diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/mediaMovie.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/mediaMovie.gif deleted file mode 100644 index b5f91ce010989aabe58d87d1ff001ed333006522..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 619 zcmZ?wbhEHb6krfwcox9G%E~GtA|fa#$jQkmAt50yF3!itw}1cs>C>lYW@fs(y9)~o z&zLczqM~Bv%$YnqJV{AO2M-?HxpQYiLc+Oo=l=ZplbxN-&(F`!&VJ_18Ch9bB_*Zk z=xAwaX=Y|-F)^{k#Kb30p48XZ=j7zn)YSC#^(7}KfBN((Jw3g$vQkP)%FD~Ex3@Pd zD=RH6t+TT;BO}Ay+&nio*T~3d-MV$^>gp?2tmx?IxP1BY+qZ8`O-)anII(EaqId7! z#m2^ZdU|SUX>HiBVfO6VqN1XcCQXWpit_RCS-yOEOiav@B}?}1-TU?H*ZBDOy1Kf5 z|Nb#BF#P}jA4m=!p!k!8k%1wAK?md{P@FKZpJ)gO5K9nEbi?UMjlo7e1!TE%)N3-$Q?eV`7X9 zZZF*ro&4Y^_3>d76PJCRq(K8?lc6L_h{z5#1&0P!W*-S@M+fJ2CKj%m9To=^Rm7RN gSOgRvG&QoHw5Sj&Y3OL4%~Pc05a86gz=6RU0QfwsbN~PV diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/memberGroup.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/memberGroup.gif deleted file mode 100644 index a2fdf3bf16cbf4ca0680bf052394ffc8fda49a33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1000 zcmZ?wbhEHb6krfw_&$l@@ZrPT)z#-SFvLy$G)GK~L2{Br7dwN&R>pXS*J)vEB_&@j zTgDKoZK-DWt+Z&ke-lhH8qBFWUs!u z#GcQ<6@FpnqtdSF=SXJ9D0=&$T?R6Ovo)C`7CH*TB`4pyuPwVd)fbiYOA zp1iEn9vR2H!_znGd9O6Bd%YnnEakBGS~0w=+Q{zLuO&nLEW3JzMo3}T1o0f)f55u|E)|MN|-@ku<^z^Gh zw!i^9dpX+$np4C$L(c5!YHU1KwDw=Ms8i;Qk{L3tduN!LIjz-6J!~@bR?92~hMQSg z8K+&0vh?nim+v;WykB1Wq`m#(omY;9ryVU@`+^I8efp%>&i~`_V@{KmXS}?2D9Q0V z&F3;ZoOs-!_F~@io{87z%&Fg2WxYw>p!>Dk4h_$qAI4MFpHH9eRdx3M=Ubwa8TcBR zYiCD2?CJjh|3AYhphgHN{$ycfV3@?912PPhCm1-6GN^LOcx+hU=o!|;V3{#tqAZ_* z1mBxO%bgfagxKXGlad=$^w=0(gbdX>g%wK`UMey-BuEL1SS45lEOwOSS9u^Hla%N- z+ggudX5oPau3Sx(Q|1UTxb^hdbL2@#I0~>&)34xboFU|Nyo*PN&8_h9@)I-pt%YJ8 zK5B6H%T3y{;zmHKACskkQcL7#=8227TRAigoSvU#R$*GXr_%ATfQY^n)0fW89;Xx< hn`&}6MVdW1orH>fA~v)qD4V%l(a4%~F~Nbs8UUvE5x|I6Ez5xwY4I5;w*X@;8w+>$L<=Sz#=1-=sQ+&7MVv#& z8VNN8uai}sJ3WgZPn|mZ`t?WRdhzbfZzjz=VCt}_xqacYXYch>o`y$FxqJ6%%j(b3 zQ{Uvb?B^Cp5tJ<2tF2Y<8qzy?udKzAU%&oHsx_X_)O8D<_2cJnt)Sh@Rvw9%_T}@J z-wG~!-NWWsxlJufnRHx3MTM%JokpM3us52pHfh8_{hzahNH8#JiK(_;z~oa z>({QiH$8B(tuf7d;MeuWGE{&Kg?}C{53w`sq}_T^UBeaZc)olo;<1T+v=wuyLbP!-Et}_CaqudtG934uNHbm zTxY`d8_(s;XEbzd)AHXbBwg?z*i|p`@UaO^|G|J^6p$DKia%Kx85m|V=z#Qr@&p6N zMFwL|k%-h&;v9-eJTVpl2c_(%hv$ zGu(XBSJk--`Orqw>d diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/nitros.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/nitros.gif deleted file mode 100644 index 029b441e768f88b84c93ef44a51889326571671b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 525 zcmZ?wbhEHb6krfwc;?IS@9*FD+ZVpNfA`g{{8y=|Iy@ktLp#XXk45Wes-Rl7GLv^C_1-Ili(PyPM=?fJRW|9gxsE}j4T{fn!)+W&8KA90cW^Lp3g1H0a4ss7uY z{P)kFzdyd6iPHODtNgRw^v{Q9&*!y%-rx3ZUE-VlTi>myJ>FCM<@%++Qyq^MM7}z8 z^me%9$2A52|NmzgltA$(3nK%AFM|%qb)Yz5V9#psZE9|5ZEMlf=5B9MQZzH_X%=)9 zah%j7>trRWF@sgplts(N-AlOLT%CznNx?N6}Lq|Z(PQj#I-r0v;!h=^xx!pn1 oSYOPL&&_%UqrEpj!weToTZ^ZzRp*7#?#nT5{rUdmCSzg^(aAjlBnjW9FT${_A3g5nd_T|OtJv%neY4)35 zqV{x8@q}#YM~9m3Ts=O$K>60z;xBJ5@7%QT@sXClfBw`(2_9dT^6%fjvM{kpB_{J5 ztPjkKT|A@Z{MwAh9H&S7%ip|w{N>HPH%BYW6O3-`DD2Hqzq}^l;_CDl&mR8!`}^pD z?VG0r-Q8ZaYgW|W+0m!hWUpJjY(=O0t<4#Ke|PSe*X>sIypb`_fM+P3!R^(nQ^w~BgaOv!Z z!;2D6>|6il)$`&Qy*<;TdkQSy-QRU!Zp`;Lmk-Vf`1SSG&yNp!YNBexxnAE~{pi8H zWCxZVQ$mi<_ixNFyuZ8d*}lU2M;gA~?7evU(5A^D>n8`izOv~0rt+(+Q?Bo;**_=d z=CNfzzr0=5?Rjo>{LlL{*351H_v_Q|FOM#6E?C{|)n8({Y<6>Bf$`KbSJjgj)e6qn)g9xQ#&4Si@Vhm2~-kyJtk&(d~05x@$2LJ#7 diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/repository.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/repository.gif deleted file mode 100644 index 35fd5a8f815ca2151ea999ec269cb9738d5014d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1062 zcmdUu?N1s90EbUU9lDp&+I25o*Ezp+W@$RJxb%z7YNt(>C5tt+OAR)3?Up9hwOdT9 zs|e*q6QeDglNS{Pjyvw)pq@bC0C|(+*oIbPw*nq1b1>#yGFOUnbnX6&J>P%H^ZW38 z*mAt#{Z0Ca?yK7Y`JVIeXSCc_UO<{{&gsY$L-Ac$EkmL`+QOeQ<2(Qwc5Btt`YoP<<5 zct4RyrGB+LT?|v~z1pYKgM@ERp_m9qA{Rct7^i8Hq6P#a0|fK=!gxHPoS4Kgi&Cjt zT3WWdh&wLgmh4-lYBHTpU+cTRy}iA$v9Z4X=M9l$@$RxvEKbri!!UPai;Ih~OWl|I zuM0rfn26IL1arA(=OfWh-g&#j+1-8F7YO)*p_{k9N6cm&2uGuo2{YYK(h{)*v(A)? z%*d$BfEcHyrkyUrX0uIewPuU8>&q)<3l>i#l1W;wP-QYXolcLMEI-C#KgAZ3tE<^; zHlNR*Jk<^wVAzNnO_<&Qha(hIVmxy`i`A-9sY|7jKS=UEyJ$9HgvTu%9QK6*#bVLs za4ak=P!Y->@H0gQLeRq2Bi!LySy|x=gmT4{**b&I%;5N}#~%a@kj-JI=@e?lA{2ES z$GM&7W^Fc&Ru4kPT<-UiZS4d>xCpmwO#UGKAP}54K*(G$G+KtWQPXdk3}&&0!=w%}kdbI_f4_K8A|F@GlcZX&^Y{ag z{{#5{Q&V7-0e~B*dwS(*69D@$&{w(nm3LlW0mX|w?{;zd?^!uJthHRKtiHCcp^jIv zG;;XFY3?}B+bDasoXJ>m&{y^HM+cYHO&5Nse()?bcga7++4$6CR?DlPqQY-ms(o))^K|0-Mh|v_v|k}Dty#Mil8@} z-U>JPDx=Twj{x>60gH`%x(`rRaXuLJcI=<}OWo*W@2hAF9ZJQFW9GGE9skUYh(}** Y6qbk8;RMzx4_<(UJoaDXHK0`8p$X#fBK diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingAgent.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingAgent.gif deleted file mode 100644 index 8d726fcdf47c6816f65f58aacc3cf280ca84f0a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 145 zcmZ?wbhEHb6krfw*vtR||Ns9tPMi7k!!P5RXTEC~~N?6j*31A{dH73wmh diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingCssItem.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingCssItem.gif deleted file mode 100644 index a697b9adce70edac1486044d918d5b718749cd9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 299 zcmZ?wbhEHb6krfwxN5{OckbNp-@pI(@nheC{c5VJSy@@OHrA?|Ds63TpFVwZc6K&2 zG<0@$xOnlRgM)*!gY&xe>#ttD>fz=2^5x4fU%tG3`}W?wd;R@=H*VZ`{ra`3nQ2g9 z;PdCtKY#xG=FJ-)Z{Jm`R{i?*Yr%pARaI4SadC->i6$ndf`Y=CnOQu1{O8Y~v$wYw z6cn02fBw{|Q@3r~77-qH{rdI)|Nk=(2NZv@FfuS0G3bDlgZ#w6=HW22z(Yr>|HP6a zO%VncpGbyni9t4f?3*7y(s5|wSS4bt7RJz|xj?E$#LZ{nC$Cf~HV$Q%EgmMq$~r3I R>>TR4fnD7_z3z?-)&MEOaj5_R diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingDataTypeChild.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingDataTypeChild.gif deleted file mode 100644 index cc557f784bfb2e4908d821fc9602fe1f7a94dfb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 144 zcmZ?wbhEHb6krfw*vtR||Ns9tPMi7k!!L$ipVnQp`2O|yub&V9&jg8s`Saz=m+#-d{QUX(?_Z$6hj;JZ{rK_m`?t?;-@g6+{llL> z-@bhL@bl-JUq9dc{Q2$2kN4lczW@65-Ip)#K7ao3F{ra_T-MYlY#0?uZY}~lf*4B3Y`t|?8fPo00_>+Z^fx(zT2c#C{CkD0!2O@Jk zdUp2di)0;3IXUmbikzEMmWJ^+DtxRt5qBoifo+EnN3_Na%eX1w8xAHj{cbxxo#W)? z&ckYDK3Ybsx@|H-g;GX(iOsqSl7jLEQ<;1EIF)r-xkSVagw$pq86-}z?LWk|+mA0_J0m7IQRm@9 L7Y{EVM+R#E3x~%i diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingMasterDatatype.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingMasterDatatype.gif deleted file mode 100644 index 2ac4ddbc683a2847f940c4d967320f87913f0e9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 587 zcmZ?wbhEHb6krfwc$UNP|9Jk58#m_6n}7Vo$=P$~9X)#V)alc+=FYo(_1dMYS0_%M zdhhPTt2b^mx3;a{xT&e7<@W76ihFE<;K9t9bCxY%vUJtT=g*%XKY9A@-Mg`|@%QfCJ8*(q4n?8N! znzd_>9zA~g?D-ovZr0T|-MMowB`y2fwHw!N+`M$@@}@0YyL%_hoxkApnbW7voSrv- zVO)ID{sV_|^NZ3mvR}M>v0=le!-tPNd-iP7i^NS|NsA2Ro7j;dNnV<;M}?Mn>TNbiBCLs{P@njySw}P=FFWpea5Wp+@jyVe|Puv zuUxh6=&|E#*Q_~m^ytf%FKer6XU(3!eC4WbJGO7zw!N;g`RUWA`T2z_RxV$%Wa)`} zuNN#>2sEF8T%h=qg^__Fhd~Eq04Pov*jF^<-6|9&nIC+dYz9_z7%nELsV|-3;=I?uBs)IE&QK!2-rvjg vmXN7{vZMD*EzzD7gClMp!aCeh1s29mEY4=ECNnrTC;5n}i953~F<1ite~cB* diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingMasterTemplate.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingMasterTemplate.gif deleted file mode 100644 index 6c79a948fef3ad6b4dc922ccf7c72f9e6e78e8e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 235 zcmZ?wbhEHb6krfwI3mjM>({UK>(~GI@nijr8NYx3Ub}Yf&!0bk-@g6p)~&yP|Ni^; z@7uR;>(;H?K7YZ-OP989-|_$6y*1OOty#0?zO|oltOOlFME@`<% z#1jw&4~`xbA-3fZxkQ1Ah>9phY3Wu-lL)m3ZG0PP@rU)F^J913yl=kwX5Z|amXsK) z!XrQgs3Aa6QBiYqv&;1Wa2DXJ2kx}Yz4sd%8-dT;+S&@Z>bABvP+JGQKER!8X=wpf zesI?hJpS2C1*r80Tn`EyK2San)cXTD_a4$$91dT=;RV(HrluyiTbOYiT=xU+4&zSq z@>OmGAOJeG9#B#So$Z{Z*nfoD_YoMWf(}W`n0zw=k zMq(sFiA=6kDx(ppSRxjQr6P$`AW^DiQngr&JTeqkvzr#HleLMDYQdET&0<+vEfP5# z4xvLPr0h#X5|v8Db4aBEh!8L?8;d&yHfD4?gO+3nyP0Oqlnvn-aU+$(YEY>3?-s0d z2d#|}2HO!5lvU)!X^})I=38n9>U94NwOTvS44X_o*88u-Osb0}Mad*X<=6=l!bkI^ zXfw%~!CAM% zq3#lBYkk<#+|>AI!-M;O_g^@2-A( z<(taOmo9$&)rBw5pF4Zz^cSZ-KY8N#v5NAt&)i3ke0sR_P)Tu7;lTs@_kHs5M<4Fp z^TGSO-`llw$Gh*m{nndrY=3>*Yg=F4vU$_S4eQqxT53x2+&Qyn>1NJ&Hfj2_#Hm^= zA%4o_Nt(EcYE`T9WdmPcf+TD=Beixb2mgN4g(gwRt8LZbWj&UZ=C?%VVm@*=$v$jMcq TVUbDxQ(MU>asGgAAz diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingTemplate.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingTemplate.gif deleted file mode 100644 index d84b0243c1b62c2627436f93ed8865552d2cdda3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 252 zcmZ?wbhEHb6krfwI3mHYcJ10$zkU05e2gXTDfvN( njQ;L9c@54iX_NdW2kY~&a56HovN0Q)@-JGXqM@au$Y2csmLhXu diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingView.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingView.gif deleted file mode 100644 index 74f64b87ce1b8b8dd729545eaaac8aad66fa0f1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1644 zcmZ?wbhEHb6krfwc;?5jcJ12VfByXY_wUD#AK!oe`2G8LM)j1xfB&XcPs?wfpHVwC zziCER-PFH-|NQ^|IliR-`_J$F^S1o||1G(E;{X45z5&7aSGVq;-L-4i{^OSqp1$$r z+n4mJ$?raX`TzfILG$dc8LOr*-BHjqJEdYuYQ>bK@`;(X)AE{T?>%;A*RI{UjWfS| z`<_}g^~bN@x9(s2|Nq70g&Wgrre@U4xPSlt`D=HR$|nB${rms_55Io>e)Qv4q}24xJX@vryZ0+8WTx0E zg`4^s_!c;)W@LI)6{QAO`Gq7`WhYyvDB0U7*i=|m)9WTXpJp<7&;SCUwvn^&w1Gr=XbIJqdZpd>RtPXT0NVp4u-iLDaQr4TRV7Ql_o zE7k*hM=v=)SHB{$K;KZ$0OTc@LSJ9}N^^7Js*6j4QW5UOYH)E#WkITbP-=00X;E@2 zP`NV5ssbzLqSVBa{GyQj{2W*)24v)yt$x9Sh)g$qp7K}fu*U7p`ojhqlLMFrK_uvi?OMhvy-tSOs`9Ra%paA zUI|QZ3PP_5PQ9RnkXrz>*(J3ovn(~mttdZN0qkL`Ox$iU#AzN>ZwhX=7~#~b4|I$^ zC|Z%C872fwJ0K=J(E>T}WS^P`OzTC!M9ujB-(O%h|M}zlx36D5fBN|0{kyksUcY+z z;`y_uPaZ#d_~8D%yLWEix_RUJwX0VyU%GhV{JFDdPMZ`!zF{kpYlR+RD-nPikb&{kYr=x+;1h3v*zS4?}jAkrS3Wts``Kq&+$ZM&Jshr=*?yJLRWNxb_ z$8p=0)tAFYSwPm5*KooPR~KI;U29=}4Lh07N8bDm*wDlwD5I~zu*7shr+_#&k3>R& fqR={bQMMNb5f2FjxZsLGEb) diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingXML.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingXML.gif deleted file mode 100644 index b7b1e5ca967c6a451ac72c7ed12b20f0615ec26e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 268 zcmZ?wbhEHb6krfwI3mrkX3d(QprB2gHmzN|_S?5_U%!6+@ZrPz_wWDw`Sb4GySH!O ze*F0H-@kvKK7IQ8_wV=b-+%x9y>8vQ&!0bk`SRuG&z~DMY}mMQ({UU zV8DO`6o0ZXGB8Lp=zzpPb~3PbK4|Mz37b1jAtz_a-hwr2*Se*4JT}l@6uIWCL-9^W z1#5{3rZpnQk{nInx%oK#x(s*TVKA9|NV$O1MRu0g2kn_Ga(>1td(%vAH8TBlOJ|hf zE*DN@ajlILU~3f0O%e~SlVtDWZHk!SB*HnFyIYuL2D3QVY_=&vtTXgl&C^3nqy;x^ MQqj=TQDm?N01bnD$p8QV diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/settingsScript.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/settingsScript.gif deleted file mode 100644 index 556ac66c30335929e5d98055946a4ff77f1ca191..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 553 zcmdVX`AWiY0KoB|T4{%6_kAC9-}l{bQ1lqRNtgugzy1s&qo5ujLAR!viyYQ469N$= zGmF}En=G47SJYedd4ex|H#XO2=e7*M0A3N0%jINQmLy406q=^}e!nPwW-=MC*Bc6j z7=|H9G9HhMqWHnEhn6Fr=bcWc-ENO35{mL23PfrobdrJ@w z=jD~#?anC*&kJ{VbmTdT<9Hwtcz^%EFf5zRQWO=9MqgrYHkDforGh;)AqOqmyQ3e$URFw`a`~V^z)>i-k diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/statistik.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/statistik.gif deleted file mode 100644 index d526390307f6d019fefd712ab61ea5bfc0e0f976..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 305 zcmZ?wbhEHb6krfwxXR7|1poj4H%^;rJoC)=ug8D=eE5GRNN}hC#h)yU3=HfHIv@vw z{KUZKcc8hzLr0F~#1bJ-5s`TT8jcrwqLyDhcp&Nk-`ra(u4%L6FHC;5UBFD@2JjG|Q-Zcl34HPN!HZ`P4=qaGJ3Hp;#)2ywi*{|_@aA&o z#nXqD&TinzKE;r5`tgyLXZs4zuF1A`X*<6*uQ+cEA4n=g;pikLEP{y}vl|(Sv(8k1d(dloVJiMv*=Kt55y*G9g zPRN#i{O$j%AOD1AJZDTjdvAC7={4DVrboYd`FQW_=v$jJ%<|VBpYMNjcZKx6AFr=0 zdbGd%-ThshMQ1f@E(@Rk|MTMmbEl?ttCw*%-7Ki9L8 z=jw60{x9O|KUi)3e0@v7&P@xyyt$XV;jHM@uch4+vL?*!z4q(+z8Vw#g4LZq`{%^0 zUD(r>YqMryx5ba2b`y^C=byQ}sc`R{*hhz&md$P!So-zJR_0E{{H6jmCc#Aw&&d4 zUbJ@W=1*rDFK#aQ`Q`2Fo2&o*`qWaJet&n}hN~Y$mwjH>=gV1o^}xK?bF1TJHhj6Y zwfM`6(;ZcAHT_+-X?uEe)c^hamtS2S8Ev?EN|3;W7Z7Z#$wAQLnNQS`@>> z3SAQB*v`IXQ^2a^!zjt{%RxY?zXh3DXtDv^_4|V3f)Nc?a>xgQ&4!g@|zI0LU?$euq2jAQBeRMjtQh>>MZp5Ot?%ATQ oZka_B+8Yk?8M8V3WIo>6&&9^O$>MWX*oP+FH3bVy3=|lw0a;tV`Tzg` diff --git a/src/Umbraco.Web.UI/umbraco/images/umbraco/user.gif b/src/Umbraco.Web.UI/umbraco/images/umbraco/user.gif deleted file mode 100644 index 5b67adae9037eda67e1154987f0f2b8202f5f81d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 998 zcmZ?wbhEHb6krfw_}55HNTT{+$L+iPC~6kJ@l%3>lbc; z6z#yhl4^~;Q}&%aeNRZbK-v4GR?zOMh7F#@kKKZ2iR(=G`}gm%l}CR3{4MXW&7$ay zw8ir4H=esSe@dx8tQvG#TyL6H@yq;{{qj0>rdbcP1CND7&2H)3a`*02ZKJwLGY>>h zdn;i$-7jc@n%90A>owjTuRTMiyG1Rl?>*2vd9RH5(!)n?8m2v2u;jRme(&aO=f%|8 z-oE{;;C|@$?>|j_yEVeED40yqi#)0C&>NVsOW$pox${(Mlll6wXL$uu)IzWD3a0vW zzBYB8I&aY-*Oqs3Ce!sI4@XS5>i+!Y_ul>2bRsWSH*C@~X>zQ6Yvnf8%yW^T zWRa*`v5Mzb&$f5&VRMvy&q?S_Pi;DU>-GzuqEp9?U;qC7*TY9|T$|rXsgy;=&S>b^ z=H@p+#dEKUN$d6NkNmpcD7frRs@eDK*?U=wB??|A^;4b(_rFtjS>Lky^Z)<<8AbsO zLO}5+3nK$VFM|%qY*3zH;5f`6%_$wvRLZ`#QB@QAr6#`yIPNXX9yDU<;pXS*J)vEB_&@j zTgDKoZK-DWt+Z&ke-lhH8qBFWUs!u z#GcQ<6@FpnqtdSF=SXJ9D0=&$T?R6Ovo)C`7CH*TB`4pyuPwVd)fbiYOA zp1iEn9vR2H!_znGd9O6Bd%YnnEakBGS~0w=+Q{zLuO&nLEW3JzMo3}T1o0f)f55u|E)|MN|-@ku<^z^Gh zw!i^9dpX+$np4C$L(c5!YHU1KwDw=Ms8i;Qk{L3tduN!LIjz-6J!~@bR?92~hMQSg z8K+&0vh?nim+v;WykB1Wq`m#(omY;9ryVU@`+^I8efp%>&i~`_V@{KmXS}?2D9Q0V z&F3;ZoOs-!_F~@io{87z%&Fg2WxYw>p!>Dk4h_$qAI4MFpHH9eRdx3M=Ubwa8TcBR zYiCD2?CJjh|3AYhphgHN{$ycfV3@?912PPhCm1-6GN^LOcx+hU=o!|;V3{#tqAZ_* z1mBxO%bgfagxKXGlad=$^w=0(gbdX>g%wK`UMey-BuEL1SS45lEOwOSS9u^Hla%N- z+ggudX5oPau3Sx(Q|1UTxb^hdbL2@#I0~>&)34xboFU|Nyo*PN&8_h9@)I-pt%YJ8 zK5B6H%T3y{;zmHKACskkQcL7#=8227TRAigoSvU#R$*GXr_%ATfQY^n)0fW89;Xx< hn`&}6MVdW1orH>fA~v)qD4V%l(a4%~F~Nbs8UUvE5x|I6Ez5xwY4I5;w*X@;8w+>$L<=Sz#=1-=sQ+&7MVv#& z8VNN8uai}sJ3WgZPn|mZ`t?WRdhzbfZzjz=VCt}_xqacYXYch>o`y$FxqJ6%%j(b3 zQ{Uvb?B^Cp5tJ<2tF2Y<8qzy?udKzAU%&oHsx_X_)O8D<_2cJnt)Sh@Rvw9%_T}@J z-wG~!-NWWsxlJufnRHx3MTM%JokpM3us52pHfh8_{hzahNH8#JiK(_;z~oa z>({QiH$8B(tuf7d;MeuWGE{&Kg?}C{53w`sq}_T^UBeaZc)olo;<1T+v=wuyLbP!-Et}_CaqudtG934uNHbm zTxY`d8_(s;XEbzd)AHXbBwg?z*i|p`@UaO^|G|J^6p$DKia%Kx85m|V=z#Qr@&p6N zMFwL|k%-h&;v9-eJTVpl2c_(%hv$ zGu(XBSJk--`Orqw>d diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-bhuiness-cl.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-bhuiness-cl.gif deleted file mode 100644 index c21ba30b57efa757213c063811fa5b4239d3c8e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19424 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 f@sN+aOn9scCH_!Rbd;aqW4+H=^4&w%* diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-bhuiness-cr.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-bhuiness-cr.gif deleted file mode 100644 index 2027ec37563391ef1c4ecae9d70a4479b9cf0c78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46684 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�PS2!a^AW%EQq~!xjryt|@ddv3s2|0q z?e?~A>w;hMivJ&&pZ0-o`{Pt-=gay^em51<^c623p|R-EU-8Pn z{7uXseA5{e|MOtYH@czo0gvx2v=DX5_wmJf`o<=h*yG*8IoBUS%(>wlUJCBkCT`Pd3gw`tE{Q4 ztFEuIv$U|WwY0aapjUa2q==l1j*^JQj+Vru$Gw8epv=A!&%vY36Oq=^#>;}!&CuGz z;M}~vl*iHI>EhppxwVY{wYR9axb(WM`m?~uyzHP9t?L%h8?}DT8ueqAte-GrrzWYg zv`U_;3AZd7igjq!1XdVfeYBM&%c)rBOujP~aw4cnDm5N5=_{4VnZ;;!+~yL2PE|Kk z6mCwG?BrD1QljNSo2vna{~+h8z?K^tW;i4z0wudURSfSq84f@NoZ4H zgUC_MTCUo!V-FJn%u|wW&AuJ?9!ysiDkGiY5+llp5}&Pos>+IuOb;z(Sgq(`RqRxr z;c`+5g04%dQN+-k6JFgVu~=l8ttq;m*cevp*|KX^jO{pb&(|Ddx7OYBr%Rhgf1loM z+cwAG!H;@l-m|p-bk35nC(FKi;or||yT;CZ5AxZ|Llg_v3zXPg@H)?tus zYD$@p5+Pdu=#na$x?8D-c2s7fTPnI_kF0iSD|A&pxtp#+O2g|rzRKF`qof|VC#u9Q zy6UWrBD>SGu0p%3uwfo6WUA5%>#H=C)-=hs%bf<+k-G7e>9!-vQdSI>2 z%1e>H_!1l|$nX}N?6=-X46VoNhHPwv?7|!&$NQ?RsJkUw+My#C^1L#=w*norsW{ft zi_u3b)HBaQB~8tpIb%z+)SRxVX2uz(Y0EPWKbfw&#C?qJ#95nW^nj=kf@vX0FTJ$W zBmW%#>%4IXopdl4($sUCY@@=oLU4zSGtCSCTR7O`)`Mx{R#U9C&I~hRamosR-LS)$ zPVRQveN(czTW$M&^rD2rsw&imrv3Qid&Ajz-)5gPv&txENV2l1zlL_&n3m+a>ssHv zCMmTa!Y%Ez*FJdHGV9&Q?tc>#y3(Qp@3PEwPn&&|aI(HQ)@XJgamo&JNIvD|V~#nu z=3DIiW3c~i%;!wMi$1)A-<%!Ih^D;||P-8}4g`;;uzcRKpyX_qvfYizf0;^C^qh7#;@#|PK--uCWyUS5InRT(I8g}K#y*3sP<<5t zVc>Ka#>4RW@P`m%oYpFLKpbxHg6x|F=4?o|bLEYMziXY*czC^(&5nuF(P6Y8$i*%y z?tvG?;QcI^MD|%u8Xoi@U!n*^@A>U`HC&?LP}ssa;;DOh1YZOT=r<7}(28<2-ouPo ztNeNKkcj+R7#GRNMp6!95u4!@^Vq2)7Eh693|a9OSSgA*5s{EoHDPOz15eB~cCsL1^tv6hJpCMg|B%wmF4lB!hYs4NLfT@o^V z$9p6zlNqsTe$f)MY#%m}*i15}QkdYBWiiLeNMIh)aa|N6FP~|~ZbCDh@EoT9BgMH* zBw{e0;UuR%8;Q?+s&k#Zbmu*7*Uv=m^Pt=;s6i2`!Eq{7m<%PRJ0BX%h+34Qh_q-# zIl$45di0|p4XH>+O45>=^rR?FsY+MM(w4gPr7(@DOlL~dn%eZHIL)a}cgoYA`t+wj z4XRLwO4Onn^{7Zqs#0zGho(C9sZfooRHsVSs#^7`Sk0>M$4lPwn)kfuO|N>_%ii|7_r36quYBiA-}>73zWB|re)r4Y z{`&X701mK#2Tb4s8~DHoPOyR(%-{w)_`wj4u!JW};R;*$!Who5hBwUN4tw~+AP%vJ aM@-@poA|^iPO*wt%;FZi_{AL#1OPj{sJG<+ diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-blog-cl.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-blog-cl.gif deleted file mode 100644 index 523323d83458fde5624d8d8e6dac5b6e560d641b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18564 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�FVy(g9Z76`*@M*w~+}M18(33{up^?Xoj8`ek$38 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 P(w4gPr7#U=AOHY6|7X+y diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-blog-cr.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-blog-cr.gif deleted file mode 100644 index 7446c64ddac696992eb2305c36a14ac3a282185f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50756 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-img-ie.png b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-img-ie.png deleted file mode 100644 index 6e796ce9cba90a87c6eeef831803191f1499e74a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3915 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-normal-cl.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-normal-cl.gif deleted file mode 100644 index b338d7c4199709f73ae93f190f010d4e01548453..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18097 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 EJA#A(uK)l5 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-normal-cr.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-normal-cr.gif deleted file mode 100644 index 494adb832758f271496533b29319d348bd4e55a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46703 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
>U|H@NhrFpa59XG+tW+VrM4&8bdz%F~|u z^rt`#s!)eY)S?>os7Ot!QkTlqraJYhP>rfor%KhTTJ@?}&8k+T`iHK1^{ZeFt60ZM z*0P%QtY}TETGz_fwz~DLaE+^6=StVQ+V!q@&8uGb%GbX7^{;>ptY8OA*uon2u!v2p zVi(KU#ya+~kd3TlCrjDNTK2M-&8%iO%h}F)_OqZ3t!PI}+R~c#w5Uz3YFEt5+Sa=E zwXlt?Y-dZ`+S>NExXrC@cgx$}`u4ZL4X$v9OWfib_qfPSu5y>l+~zv>xzLTSbf-(* z>RR`@*v+nXx69q`diT5F4X=2|OWyLD_q^y$uX@+Z-uAlpz3`2%eCJEw`r7xt_|30= z_sie@`uD#84zPd+OyB|=_`nEGu!0xN;08PR!4QtHgeOel3S0QX7|yVUH_YJ<{9 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-personal-cl.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-personal-cl.gif deleted file mode 100644 index b338d7c4199709f73ae93f190f010d4e01548453..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18097 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 EJA#A(uK)l5 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-personal-cr.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-personal-cr.gif deleted file mode 100644 index 494adb832758f271496533b29319d348bd4e55a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46703 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
>U|H@NhrFpa59XG+tW+VrM4&8bdz%F~|u z^rt`#s!)eY)S?>os7Ot!QkTlqraJYhP>rfor%KhTTJ@?}&8k+T`iHK1^{ZeFt60ZM z*0P%QtY}TETGz_fwz~DLaE+^6=StVQ+V!q@&8uGb%GbX7^{;>ptY8OA*uon2u!v2p zVi(KU#ya+~kd3TlCrjDNTK2M-&8%iO%h}F)_OqZ3t!PI}+R~c#w5Uz3YFEt5+Sa=E zwXlt?Y-dZ`+S>NExXrC@cgx$}`u4ZL4X$v9OWfib_qfPSu5y>l+~zv>xzLTSbf-(* z>RR`@*v+nXx69q`diT5F4X=2|OWyLD_q^y$uX@+Z-uAlpz3`2%eCJEw`r7xt_|30= z_sie@`uD#84zPd+OyB|=_`nEGu!0xN;08PR!4QtHgeOel3S0QX7|yVUH_YJ<{9 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-simple-cl.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-simple-cl.gif deleted file mode 100644 index d7012c94b5196dc08caf5818ff7d34a45fc83899..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17705 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-simple-cr.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/bg-simple-cr.gif deleted file mode 100644 index 1b069f2cc1b0c0c6961156222cff6011c2b9ec96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42137 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-blog.png b/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-blog.png deleted file mode 100644 index 4e2fbceba879a0ef6d28691e0a199643d83a7259..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22650 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-buisness-repeat.png b/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-buisness-repeat.png deleted file mode 100644 index 3f67905e55ebe87f8c55791acfae029489c7fabb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16942 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-buisness.png b/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-buisness.png deleted file mode 100644 index 3a1ed20a9777ece71f3bf3b8cc0f00299af1133c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26481 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

g2E>a654JG?4w^?XDMRSMQ9;@m=*rkl|Ww?kd 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-confirm.png b/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-confirm.png deleted file mode 100644 index a21c3c0198c3e1519caf1c6dd9bf55f972726ce3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3113 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 D7(^Eb diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-create-hover.png b/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-create-hover.png deleted file mode 100644 index c2ce62970836524c1727e273fd5b04194f8d4f64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2243 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 R<5~a!002ovPDHLkV1kvJUR?kH diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-no-thanks.png b/src/Umbraco.Web.UI/umbraco_client/Installer/images/btn-no-thanks.png deleted file mode 100644 index 810f857f0afcb96b03a77ef92f333039e49fb00a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25962 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 z 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

lrbgAd-LHgy_rG&CBzZ!g;&E+M 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/img01.jpg b/src/Umbraco.Web.UI/umbraco_client/Installer/images/img01.jpg deleted file mode 100644 index b86b30f7ad6b5db4c958c208e47ea22a617a9475..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3189 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/img02.jpg b/src/Umbraco.Web.UI/umbraco_client/Installer/images/img02.jpg deleted file mode 100644 index 0e5669be88083a502161ac12729bf43714737cea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3533 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/img03.jpg b/src/Umbraco.Web.UI/umbraco_client/Installer/images/img03.jpg deleted file mode 100644 index bd0aee6e12ced8018cd95be8c1fa9e92468c06c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6205 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/img06.jpg b/src/Umbraco.Web.UI/umbraco_client/Installer/images/img06.jpg deleted file mode 100644 index 67d09a08ce46243d7a2b36999171e8cc0dbe4b99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4408 zcma)8cTm&K7X2k5l+XnvKoAf^mEHxV7wI4!L{pc zF(3lctDwTeZ{~e}zIkV7XYW0C@7cR|W@q+d`eF&7(Nw#u27tf-V1GG)i%o!5#m~+) z2mk>@0078cQriHPqP-`|0kChsWIz{lKomesL_`E7B8EbV$%ro}IVmwQDLDlUMh=5f z&`|sX8VV|E8fq#EIy!oKIywXk3k!nnUw}wRNGK^N;WRXGW(HaY=6?hKuW`{1(2@cV zffxvg768+NAhe*1ZlDYRzz`4!bXn(r0Re%Dpu{AXX{blY2iB?!nmy1@G@lXz?1sKe7G{O1Y( z^f~@j)~((8Ou1T-8T|+rys?nS*iegPnzdfWseIa#-Da)d1!ZApIpN&C%}<&e-aPgv z#?P>EE_temV`knudaI|4I&`CxZ+(y-_?>5Znk9Jta{ch=9QmbxLGVS)db4xmysT-8 zjJK0lp9MxkT`Q<|JN5v!asi0#yEkWC0M6l_4ew{3{LcSW*6xpo#0_a)VF;|)5X#AE zzHcs-OsM?@{qzkky*LJ{hAZ4ifd}R z>weVn$)n$2W?iP6?TZFzm4;~hr`(pSY#rO^P0MJ$uS~G34O;)^u$^P_k2)J2Yw;&N zej(rH>J|I#9Q~iUTh|^1nFp6lrf^)TfxKNi0jb!F_9bW`2Z z=>?E89Lf8V*L30hnVx5N)B&$4r^)!|2d@i1jSrcc9*f*LhHvhyP`ZzV+0l`wIXDky zwYxK{^&cE4vwqerbNL8I%H`2}y0j7A8B4UK5%{aP22E-jVmW;zpD4SrFD9At!@w3KdP@IiG5uhYgO(3WZq56B$8mq(sixh zLW}%X#}Hui4k59&FDhlSVA|B>?^ks{-2A{<0)^Pr9q+Zm5`3J_h@+PbD$KiX3N{x! z?3}^d3%J(Q^(T1qz7pbm4W501KF7u-p$vkV@@B46+c^8YY3CuY$EIH`d|o&HZMbQI zTvmx)$-CU`+l!Ux(n|RkFG9DM9%wa&ZM!A8CbFfdwo^zlW79`062qp4a2|C~S>hiq zP(nqM0JOB^{d#qiD7kkiZ}be3!Up+D3Ee_Ltuis69XTO&T>qWd|GR;L!kr3)(x&P^ z2_2_G+P{r^r^_09O68I3&l4Jd(a)nM*wPO_^;m@_c!;=A_dj|_cy*^o3SLLDGAy=d z%7(%wS)}NAo`DGDxz`JLUijVv`wa4w@Eli>St z6`u(uVQst zUO`TJ3E^AMUp>c7eO&Qz ztjE-^3ni>-F8}lE_FU^N4=qI(>e(vOotf>0_3_JG{jf=wFJWK&cXa>C z$NwptY&&gRU(A<6;_6s=ezPr?7TbIO2)LOn%=|&(fVd9F4v={P_xzQ*E%%?Nygz9O z|0I9p+`@qZeU)c>^7Lr>0=N>sF@71R+I^ieS@GH$<#m*?$97g{Vt%zd^aC6<-#fx; z(9v(5-S`YE_C;Kzi_@N{!OfeAWH|CO@3LnGt`xUqHl|Y81>Wj_;b=!!UZHi6Cg*W= zjAHxUh^%IwqlVv>QbuTuFMlA5Daz67Js6(Fpy`6bT+rCLR{)Bbnn+FW(9*wTpocWI~XTP^K*otHy zAy;H~zusKBi@ZDEGwms10gG7`>MVe5@+cDU;TuJ&&BO#@aC2i3X90l9{limF_s z9a=7OGqgOC`5x;kLc&l$cjej#`SLpn>{OV5}d>O z(RQJ16mZPn7nvw*=EbGIvL|0>#(U>Kz6JxQVfPPM@Qtu4>V8^}Yv>dt;3@Q&!Q<`_ z*rUjZO^6bN{JiCQ79hx8OO$Y37yd%-kZ85lC-TH5c5IVus-{C?0>1?2 zk9XWzeMWg!zqJbvSIFCWwqY$LuwC8=H+9up$`7bp@dMxP6vwr9^y7{L- zUvzveEUpA|Ip{WAlVB<06>JzERPf6i5Oz`=uCH!9L`6qQ3dmTxc(t=H880u>zfa+U zsjP7Xrr*BfC>N#A@@9Ef7q+IX_%0A`si9fusp>Dp3f$ROE3z2+21Caz8PhB)NRlND z35qm}^Jc9zh8k{wGNH)4G%?vp24V7gjA^&P1(0~EVTG%MH7YLQDyycKsx{mZh ziilJZv*S)kTW)sJ^RyC{Z^ws1U9vjpsY(+-E3HEJS{?I}RAPM3z`nw7p z{`)~rq+s@bxQcXY8BsT*@KUM%?k0|daDy_~*2GtXMpwz3!ih|zqtz^rtUuAh7Ax#| ziw_1KQ78K3CDJq5zHRS9gEf7MiR|l?7>=rrY9Mu-o%iC85hoJm@p##8*>;xm|IqIj?GI)T<#^x3a2;|Di$;@Q%fnr(JjHIk51aEGWCCuyP_a}DIRDe;` zka%lN_K#Y>?*`f=KZwQS=$ggU6Zd(PJ^gUTJ<+0r;DsIdM7mjhqOmEu=_ z6vpW&?XGQtiEw>HMG?kFCZ&5rko;`|CTI4G6o0Ynvn8Xt6CsEP1Qds3pIL+t-mBvTtJ;iXmeJ5n+==LPZ z##+!?)7_8;Y=W7A;=SHAy$TYvLYzpWOaUj@7m1G%%wx;B^dPVD$1<&-t^~JndsbWk z4la1yU~qtF;GpeIrpVNZb~dD1hm`+zIzeeoYgRMTGS>}}sVyeEd1jEj^PT(NPd%iw zaN6wS>E0vtQL~U?(T{elQ%-0%eM%+TmXtS(whAnbljyp@5&f$;7=VL8@(3OWrvZlB zV{PQgJ6x+y5uzgfkDo@N__IS=ynIUBjrs2A0DCc8g|kPeUfr#~jx#!=$PC-~es6>y zq_yb2j8;#OmstPs-MqD6T!hY2P6HLLxPU%sZV3q=tZbscng059AwsXxbVm64v3f6+ zBjVK%IQfBq5mRF8{m|v6*s}^Fq?WRemt)c`B}FeH_USgwC$J1}Z?BmZS8HWO1puL( z9#7oew(&ze?9EYQto^=8I$6HP?WAy{P=?r{aXLSrX7o*4r3Sf(XRfuU{F(EuGz^RV zpkZI#F8uIX`_>O32A|B8rtwlTA+)euGaG@}W<*2>KY1_1_^x~@$LDq$i=T*b49gOg zdlcW^q{kz6t51R^X)VLaj-E0~sG>Lf@*X9u9w!=O)*}-u2Yj-R3G~eT-76S9joZQX=@Wp;nqWB`FW- zNg~P9D3Ydn4_mZM$9L))qbYX_`iAZkTe!Q$80feJmk#PGeqxl)K3dv`lb(x&$xl6F ze2DEaf-$%36MpG$ik^)pb9fEL&_kHYn%Xfx+-7R(kla_ufWsgja?6d>M*M+7FMS^q zm&5Wb6x58-nU3pBZ>%mCfoGkrMWh7So$Q=HICV9)7ofY@P$`htGZUk)1P&_QAj{8o ze3&z^`mP{DK_5n?nSj$;t{!H46>jS&5BOK_6D77rWOV zdP7YqF%ukyo*KtA&p@nVQ(q4PrFmeh6KTQknBH%*sbNxo9C=IbMyqgqXA@GxMEX&z zNG^igbxj#l@Aao+#9ZWKF&BW_nK)6?Y_>r*saCadkglF2Cd4Ki!pZ!3HzMx4qJkxV zV5|f{EbQh)n3mM1Q*fAwzd8U;U3#42Zp8G>oFJ2w$NItwP@^Wb%mWSFRRD&f_DzPc zzM&?T!^*xtWZ3N;@S5ip$@T-n@CgTHzg9IZ*+_G@UrE~sRGpP%Z+4}hr^kt9UjF$s zZS22LHd4W~RUvOztcpFLkLF`)BX9dk3AkyyPVXLLShG@myjYZ4JE_RM4~ z++7tXEN diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/img07.jpg b/src/Umbraco.Web.UI/umbraco_client/Installer/images/img07.jpg deleted file mode 100644 index fc6e89a460ea93aede6e2635b078e4762becbf9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5924 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 Q>Dv=y*%C*t3~uKC3j#9a4*&oF diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/img08.jpg b/src/Umbraco.Web.UI/umbraco_client/Installer/images/img08.jpg deleted file mode 100644 index 10916e9e685daa24ebae4dc63858e7eacc48d802..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4535 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/img09.jpg b/src/Umbraco.Web.UI/umbraco_client/Installer/images/img09.jpg deleted file mode 100644 index 723dbbde621add4c0ba030bc635cad558627c20e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16018 zcmcJ0WmH^C)9&Dq0Ko$c1cyL?;O_437TgBc5Fls>?mD>Z;I1LKOz^=axFUv&y-T>ez$jHk85D)D*Rd$YR!REG06UQ+}7%{&jpW`_4$ zfpA85*3nr2;7f#)(qBcvJADQm2EHcx0PL7(0xY-|!^x$H@&9loh5VK11AfO80@XlT z|LJ)+2F2fE&wx4lV6m}(YX(I{z!Up`&A+2i02uvcOAPIAu~9XE<=>9uU;eif+{Axt zBmK8Gk-uXtnnWMt{fCSAm;b(lcr?;~xgoQIw&WiXIrRfE7>%)g;3@&3}jhbd!^2Pl9q_lnA;(pv2uK35z21&3JNGQ=-yr7q_MW@ zAl|k_R~$Jn=^~+}66tetg}x z_61+yu-A5dff5!PF<~PRo?dbb%bN*imk9s z=K8*JQ3Cg+yx!bZI@!DTYj4NksEPZnn@t>YUx$3ee1iM>?rGTPBGZ(~_8Lt%(@ z^jE$HG~UN=k<5&1_I|AaSABTxpV=&kBC5tTvA*;N6;zJ_e@voRlXDv^x6PY4^x$}# z6y)aHLlL0OiFzy4)N)HkOXHJQJdJs-J)GU&OwY5$UuB_F9@Cr~Gft2sPAD9ud4jyT zvseYksW)XL^)Ve>P}=s{zRkI7cFa2ykJ}6_NVNOVix|V!h1h_M>9^rO)9Suyy4Cfse@?RV>UUw^;m z60V9TN}PA#;#z7dKC)sgDYBbD(=wp@({9@n1MyHzn3Bbz9X|i|a?AkXT71CDX*crm zRr)nPtOObQO5d0CN>621xVOsP!V|-#j2Ii7e=U$)Y9%sxWMV5?dt|jJlj|ZE@FI)B z58CGCD0$ixP`@?q51O?JD7Gi{2{#ejS32J8C;ws~Po7)=F+8aMWYqU!k$usQcfQt0 z)tT5sXONw9BoEaFmA3}3)ag#W*d|Rh-eANGY;=Cr^kr?oSuHts*HHLXsfa6`kH8IcW|`@q|8_WJ}`K^N+TF=bTFGqO_RIY z^(2-IkybG-oNV>>np>FH4T6+GM%GTvBn4XIe0d+6<*@<(O+)>I~Y-ci>QYw+2aTx=W7oTAe zz95Jv{y;;9{*i$SSc7bpN7YDj?b0i8@07eF*RAX$C78|KGDJh(OZ6ZzZ#fW~8IcVg za-?(7>Rv5(6#^~0}|E8O$6sJXf~NK$S-A3^~IqNs}p^W7;J=dm8K5> zvfnbEq}ZL?e}V^>feI{-rty5cw8Jfl&oALg&l+xmZE|3>Z2$(z$o4G%O!; z7s_}j{>8rxCG9ij42>ze7;vX?+pr9sYb0C@EITNex)R7T`zdgrzSeY0j&o7c@N$&~ zhz^!pCa4eN2{9?-MN0Qoc_s6FR&$#0&zNB^-9m#bDSep=Z` zo;GEsu%)_a?xT|e#3IE1eflL9$0F>iDvD-3N8&1tf9v~U>kEtd+p6m;twpCwrEd_a z36(dW+T()2(=_qINju1BgLbJrC&RtTx<%E7{uR}q84jQ1Zr|<-9aUQuQxe!;otP36&I*Zk*oeB2Vvo` zA?`^5$@!d;cyZ5WPdjZluHF_iJ{;Tdr*ZNV_@PT zwNKxH1ivaf?S73HW6l4znH98jY7NUKG0Sdrmoz%ALJib(mQW6wpsx#{aqBvEmn-ZA zsW^MDp*gqWJBgFkk6Kda$K>@(K zNm4<1#i;wg7y)XUrVm~PVlhkig_kNagpU4Ga2y?v34aw*F;sZ(PVc6ukSgIkT$OOx zucN~OB$7CMMDPY85h3u=;^6?{FQPyK(HXu%!tX{c6aV5p4=m-w^t1|mT-BYgxR4GqF=awyvhVk$on z96v)Fjh_H+>L|QWTX;o}!bnE4Ghh@%*P6WTqV^`V$PQ1M3|}%xlK>loh5$(d&Jac@ zA~JtB=!xctF&++A5yb}C-y(B`VGN61AtOo-AO(dEp#Xx4*l^M}u$an}(ceiW^OWEw zDLCpRD5IlD1SJGhDx;T7u>cS-@MxO&@Fnx*sZj6(@$4{a3+&K~#86%+BS+?>upmc9 z0KP2+p&Drg_Gy?>&!*E;vdPg(m$GQpil+gJL*tl~1MAD;6W`j0jV7LN5DX_VLRQJ` z4aq{55-pTeoc50-e3(}gPI+st)Yio6-W+Js(PTAzgwSHJ^p{ey&1SMt8?F9?m9u{f z=a#Udl?*QGEg*Xv+CS;RK>dPRjQ*dMU%^fXpvA;NLX2Q9Vhhg0!BHcoilLgZcVnLd z=n)swM_@;k@S$O97chsSqG-_OvFqU21A`}9)NCPa`^wa&Rvi^!!h~1E$lSrqp*RT` zDC|<`Z$l$&rI-Nda!LZ4a+1ZGL4;CZKtWij+zV4HE?Pw^O#(3$WME{x!%G*9$s}4{ z-{5*h40pMI@Hb$lVHO|NAcyrj)BP4!d5 zbPD@Ld{rnaIz5V5kl4SnU&%irhJi$mOA{0tDQAX)@J*)`8U3rO7LowaIjmSkO#Nm>OCs}6v%}lb9ep*|l2-DC8LewgS?3t%4#U@I24REgs^oZ; ztW^;t?E*7OQ=Hwzo#A)$!jM$`vM3j@N4XaZP2f=X>0G-XqKu05nNIpNW7W4SMu+LL zQJLEih=Elvwi6u}6ptgJVg^|L>O~U$5%M1fS{bB#M9$3Jo=uVZMt%%2z*0~QRbYS7 zSjAi9D7mNbd-f5sj?n;zruUs>6XsBf&DA)sAPc@K)20XF?NRadME8`U7}zfqFMmc^ z=AnnIk2DvRaXq=-PTW1eDDD)?ND9mX6 zswJL3at-~&ssns~1mC2>j2hV+q6IKxScme7)aGjq9{jc+mgVm`uT^zs3TGYo?avMQ zfjNwCVEvYkOfP{n>DTk;==NBjtyD8+8@nWxI>AF4O{qVwR(8NkUc}pG%boM)Y-)=$ zS{e7xfLkisB>(#R4_wvnQx%!dg%?^|mkbkC^$nZ`o&or|je4Kgw;!BSpH|&_xa#G7 z0;Jn!DZ>-!`bdE<<@y^(&j+^)rppp>~_Ap=WW}n0HNu62JG!FCllr+Rn*8 z?^Y!y*iGBQsLjWhwA-LSb!~q;Zss)09Q`TKRm&-Beu-k4zT3_vG)tAeU2l`V+9HZP zq5ES0r}8(==F!{8D@8Pzdr94O{rZV`%vuAm3h zaUDkum@>aL1|6H2 z(_QGkYhUc_D^V>GIjiw+U*pC~ z0PHRJ$m8+Z@G~9Q$Gnm^CNl(LW36H4F^;HZonq}K+bZxFQJQ4=$wk`G_U;*A1@-k) zYP6?5kJDE>Ns{vUl4EhNt6_%v-um`XD?^@W0s6{Z89YZUx6b8RS=no6`L)66MG}uT zTe8K?9|)76*2ot2fLvyQ*y@Rjn{|9yR^)#`6+{J54S;7MDQ<_APj9TAM=qKuT@;{w zF&?1)M802BM;He5=btBW?QKAn|L_d3o-_0@$w4Lc^A}Rx(TSJ3b3(k2B8oFDLv@w->Qp<&R4+8_(^j0NYOg zlJtdbcRs9UFENXJ5_xyUh(A?!&IFFecC;CHy6aJI`O}VD=pVne#}t})cwNmW1tOJL z{%-By20&=zIQutZd#~qJ zO_*C6@%2{SliRoTP@@DAOmqjm@_x;sMTcwt)D6EA6WzT@G z^%MT?EVIx~2Xp9T($_(vyNX5 zLK1YCrw%8Emz-DEn>veM(;rP9TxYe}F0gudw?U&-6JmUe9ej^J`eHa_VB6;}7g{)T zD6wa7}6#aJ}^AD^V=fSB2?n)@sVs30g-_H(C7jA^st5S!tqAEHBKOzmMU3 z?AcytbF*O+AH9oBsMNP#`oks4EJt-fJt| zOyZ@c3cA!;aTf3l3Xv2hCSq^C973EuG{i=VnH&42{8AYfa9vn!S4zZRy8kX`WNG*yyUSiJ44Gu8*HXY=}MCh>h!=coPtA9%lTAuk4ykHJDMZm`qPR+rXFgYIt0wtAIRmCh2)f#}z19|!M^(fHgULtJi7T}4Lc zPhFJl;LXh|p!bs8Ga!2~QkO^7*B5nnzaZsAp$ne}w@N-0V{qS^~-6;80v9K}U0^839j|o2mx}vAwKdrk3PWcqd zw9oPjtoCRk^G>$wQ;@N939RNN-ic6n+iBN71Mun@nhG_*uFmRx*Br|4e#ySy2R{Xs z=^MJ6WZ<^UH3}|=pO_fj@wLBJ+O30vdMW2Ex~L;Nt?;!jc?}uRxIUf6TooQaLB?Av<7&w3w`J79a!*Bqv~_-96qEm&`!4Xbw6ZVs^Xiq>z3=1N zjKciMDdyF2S3(_!%-N#L5aO|-JW6b_8-%&_vt+%;(u#sj#}!C%`&4Phr$%P3txrMV zN2$YbHL`od0i~T~RZM|*Du!j{h_B)4&ja0hMUP&?jM4MxKBkb=D8qmoX=nriTY6Z3I08a zNQGQjv6F?1!?}{)2vV7G${%tYnR}uwq%y@73rFh?O^>Wge+&#_ z6cvN-fFzE4Ke^7#UEG1PW?8yuPvD~tBvS|$pea2R>_^*c7aO%^+f($`B=r9 z0;keadtOACjyw=avP?3{I3BQ@Fr}uzt$#ESue$wNc4CQw_WUs?x6qC>%JYX=#I$_% zA*{x|3e_I{1agS#m#LH+_UEXnYVQkj?^#}TBn=PNJA(p}O8gKWXrR6^4jZqRN3|~% z4-cl@ZB^*voYT5*%|ar3$3}Atl+T0FvL(2{`nTFUq1W$tjb{t`VJpNk>+fstU`o>y zbX~bKRm&rc+h& z4&L!PfQvqMii)VrE%Qmr;5GQK$9^f+wB0*98x+ENBd?qd{LSy~&+G@65Dc%*W2r_?iXbRllmgVXlS728no zP=pHRu>%jYQ}i+yl80wCh>8dEO;aF`*HR1GlQ0HNrp3lo%!R5(s^Ui`qF-@C3PJi{ zrPX;qI_dC2{(R@;Pmu%Q#qE}sJBx*S%hEFf;7*;HHT}6Q%$$hGlE;R|uMbwASTRnz z&fEBDCJRXKIXpox8ey5^-z%HzI^(p?*z@|dzGIAAmF`2W)78_cC#S!|_n+Om&UMBn zDv>#JnWA}L;ok{gia}_Fhs(oPigEbbs25_JIHWzfWL^ADDk}sflf2nC(B7D$URs@5 zNV1W}mS5g`28=lBhG^ACTfR9j@|YuYkJgzZPYCTpf&*a&d(J1}ic)&^AWh`qkktQt zF!qJq;IPn2H?bYh`U^X~ounAMh7(OK6-H`rkUUte#bb&_M)ZYDLT`ucX@iZjdfvWx zg_PnimVJwsMAYx_$&!IwPiz%Cn$SCujdrhR16^scPojm5__i%N}^}`J4=}Z zO?(_Js9WYk=A2x%t0zXyQq4S=zPf_=HvWbFPwR`c?vBG4ci)32~P#Jcv?I`qpX&6sdm zf7b|kd+srN&7Ap&Ode3bx$wl}teM9&Ptzb#^aKONoHG`*I%Ji zsSx=O6K6=`|IC-hzw?#I#TdbdR$p)+#UehM=K*`6?RAFH9-BB03vz2_xB&v_M#WJvaC{M4Lzs!eRh82~J2PN| zzo?UKS!RSh8CB$?!C_}xj2-sJ{Xl*Uanep&{`Mc=xp* z1m65Tq*Hk7pnRhKZoF*cFo5=g%v$V*1}jv~Iw=Oj0#gOk*J$Gq4bhAF$=?q;!z7}# zizXuDVqq6EQ2icE6d6KHi+B9lo#&JKMMVr@!kEvPz&P*kg>ItEjD=fbAgz;=+*ruU zUef?%q|eL#xL_@_Vx}$!=DmI{{Rgj~MLdTFQ)AqHC{wTxvw#x&UVp&pH#t+2w<3qB z@jA^RWcHM*&W3a`kw0FZHbLf^?e&f?*ofon@9gA1Q=rPuD&;I8qBf3}ooo}I_tdd` zD-J6#Bl&@F8M3<_M5RS6y948OZK)*pB&RZg_qRHyOS?;g?n8;E`ewl^sjfj^c6nbn zRl6YZd2yG#X!K5TTPM^MA)ynBSjny)Bf%YdS!)sUrJoX)hMdMNYqQCM%$KO;*CpN7 z4f_Ws9Bk>`cM&vE!NUs&G;z@fG$XsIQ&6g9D4BSj%zEpgcHg)U@v%n2rlH+yUD9Zn zPH~b5^UnF#M(Kpi5B;>f?|0jY?qC%^0^GrzA$2PW<9;T%EvE5g)n;HE!`8hPZsR~- zYFE1h1oMtZb&i|!&pGs}hUXmKG#Brs8;tmZ=~Kr@uvtw%A3(z~Ke`GdXxxWY*?fZ@ zwj@A$mQT%7&zdj79Su|->Y>DlY#1D2^PHJslLYIh1}TObkLwA!GmEX?lla=uKHw^H zPliqNNLYi4!92TbF%a9eK2gHtnqoWEHhX0{mT@ytRV3c!n%n2_Q1bF4< zlW5KIr-TMij%Tlz#QscHr(ClzDqY-{bKZe^kgycRXB(5yyB!=jqoh$e?6@;5ig#PP3w zsQ)^GwNZw8^WafNxLCF2x{d+Y5Cnc?wGTOcM|yfoD`3&5ih=l^VTU({uZ7b^lu6m` z_j-_V8OBfad9**WA&rxp@$<+U6%NO3>do!kqt(E_y^dd#C!((PKO{NhD~tJdRd|w< z#`?zd30EozWorv}u>Y$W7M~o}O2wW5)t^S8=MHgLg?AC6Z2pQZy;x)oacoyCpxy#$ zP1p4^1(j?Yf@gr}lj5h}4s9}cj@|~1o?4r^&Rf&XWykK&hB}ajw~OqVmN`s#+e4j` z=D$keK%f?eXn8&gnX(A{kgOi6 zO9`eUxF#Ilj?@P)VfN>rj^-FHccV z##*X3riFF2#Q|bM9ZiSFT`o$A!<&n)GxYf^a zZu*hKw%aM65#PVmDz{i+yW%&PG7;A*Sg7m-ivgi59Yzyw;U%Mid4V+ z;RQJ=sl(m0X-LXnxfoBBqvNbq7(!eGVk&cizz^xXtM6lqT8fQ*4TwvJMKdoCIC*1X zXuPyv5(cMtQ`_0dBxNuyW3m2dduTSzpKz(tOkX}KDh2CR6*-v8qVW$zyT>qw*&qd# z3@m?B;l^zrdau z)ZXAh%r-_c^;)ZqbViv*hwz;jPjLn&LU#{$J?aiyC`K1O>~E09r_#PZ(OUKqAqS#g zdl6>CZN>~3J3b98vADey>}#@ai;#j~vgRI{n@}Zp+Gud=@cawDG>eAG>Mn?Z{q)7$ z@fF$39X>BFXnIiZU}#2>SX+5Ln={{rW z_mwJ;%or@OVBw4tmijAjDie*oBH*&!$n1$3dRqT8tLI+W19fh)wJx!LUSJJRN)ruj zKw~M4>~z&YWXSO1;ltOs#*x$d^IlH(7Pg80O(-xrI+mZzEQ%eMzuF>V>=$BhKh&x@syEJWWdWoGwG|}&-k{Z7h0I#h_Ece6Q(Y-ZzN2zr8Q3#{O z)BBgua^_Lx(TLM&Zhvb!Om>pjH18YWwNsa8v9^6}ta6K&+u&SIACk;{2*hnUm|4r$ zSC^A8PG-g;zEj1xB>J}cCl-|Bb2s6iDS_YD0AWW_EQQ3`l{T4xlS)nhj2 zc%FIo`KQfi!z4@>7 z+i7^?J`modcMEURTe>M#vK?Y8U|@V*-x(_4efNFM04_zsfG#e$Wx z8Hzd1Izr;5uyLriqjkNERU^9KRLRfzHN`_v=wd*y+S00XW?5(iMEi?We3#@nZbU)uE--y#sF0scZ@eOcdx#A#iDY?*-pT|YY2+uZ?#&S^ zQhJ|uG#1;I=5yojTc~Z<9Y_a8pW4FdCeIcidGYzH{q((~o_hNGvP;iZ3dQ#3j+}R1 z{#v(K$?PM}IFW{xk_`rxfifKF+em{w@`Eg#m^6Vx@ zpp!@x5C8F%#8z6PvBuK!A%kPl?{BnYuTX-wJG-|BB@x)xe^vkR2-G8&TMv0E^oc5cFctFgOc!6Y<6&`?UqmmpH8fU0PGU zz3ExLG#1Umu$PyO>lp5R1~~Iu%2_y--Gmf{z1>OGbBm=3bal5=+aNz5aV~m|(j{`# z&QL5QDykZ=FL`3O>sQiYW?@#T2(rATz;v{>V6@12WAJi7UkTwM$YjW_d47J*qqy0{ zn?SpfCYou%4ajwh-2`<)w~}pXa%bn9Y~tfb-(B3W?c3$IzNM?8&KF$mX%BXY%3iL% zQHa8q?j6HD?|9Lhsc$(obZ1`ILR)-B(6`g%zK~jPJJUHgVX1gVExG%qDM^o92Qwi3 z2%Se(>+49|ratS3+E34bu@z543YNdy(H{EY88tqRNH~Uxwz(CqI2xJLTxbxT?1L*X z;oNQbMigmVaFdCGe*etyV*A862ipTZvc<=;O5E(BES<{P$5%YL|HqsuC}8qehBok#Fy0M zP9w&2X^#)g^$JQ_BX9m0+pgGFR^8yJzFzrbl9%c9iVETK zLQ8p8P7;^D)PyGmx=lm4svBp6m+o(r+q9zxzr*3;tbhywmC@3aLSrKK^tw)L4R z1G@L&j^3#QUlLfXz}#~#MpsgeO0nz0pts~Y=xF|3W^cLV4ieV10&w-|QU#bBN>^#vO<9A2Y>AOTA?#N-qV;u zfmZ{Ex34x0(Qjo<&ZPf+Vgoni*_sxON{38pa4Frb>F+*r%1xtW{Z@ouIb}XW9o*k} z1tK1@_LuWu0gZ_fx1Ja+HwrFUvme}mXKWNeXCAEp0GZ8dT*=G_!{B@y* zvn@bUu7Au(h})OA>Gj^eFL&alKTPxzNha=qD;8ZdtF8`*8N}2K|h- zVpq!_S<+sKt^{zqqK4$O`8*J`NP1#)10c=pzQJ^yH6!iyvWr62E+*=yW_ zegwHi!fPbGCw#ZLDK!0PsyR|5<-EXV^?;wS=0<#?>cTq42c=V|d0IB{W=-joOr0#& zcTEpTF^EPO*no-cz}TF#HZpb|WN2uCBr40m zU3+|VLaYEH+%%WuvUL;OK9ep${^Jvu%NYyHhszS?q7ed=jDbo@GeLXO-+RaXDG*=9 zPTq*iXF%gKKq4ioWiN(RPH*g9S45|ogU*u8jMI?E&TT3^g3iwN4^L!w*a6hYcb#}> z=Qg-)`-rqU#J^(TqrQ9ZtB8g{eH{9?g%&QO8=PGyuC5RBCJ;kL2uy2lBI6OGtN6{% z#0m@3$Klq7R2AmbA02j}m)1l-XWb8v<@n#ubaXuJ40o@7>gVf=)733}kXU{l**oxB zL|>AbOi<&B$Cljp-R9Q_A+B183zBAF6+`6Tn4PhZzT==r!7~RBo*0Q0vZlaaIAm}!85)u8)|S5vo?t5sbU&=UI8(};6DO~d~9@)>Z49qI)B9I3M`E2Jp{ z8*rLGHE#Fj#mdognCaGL*CK=H{nQy}f2r zr0f3`lTpx-%*TFWMis}dUFzI5-9eO{`>sGhxZkr=iXHW?ayN+_v>8Ys`D z^z*ok5zKJO(riZ}T7fJg0=vE!SAz`$Dk+OvutKkm(}u&hFj&D`Hq<%%MG zA%}Hg2xS(8T#rH{s$ay+3iq^2)HCUBeCqMcj)16G3%b`f?I zxRYdBwV0VBY|}dYF01nFOar~Y>RpxxCko^|1K_4oD07N*sx#6xaQ^;Fc0z?ri*s-5 zpRxj8SoDr}+EPU&gS4v3)H7gu2-fD*P4B2#C>sB1Rz^8VsH6iLV1EcgQu^-goYUkf zQ`aX@YR{q<2K4Vr|_{&=+Yly9B|a)Ik%v?d{@C>!6_)Ho!$~`pMfUy zAJEaZofrs6A|-tR2u>mvNui_n{iMoxxK!L=)6B-Eu-ClCUyR#rLZr!P>NN$w5!_Qh zdVi$E&jkT)Voy8}HT>z(VrXO|BC!Y{2%$BnVs{sJw2S|9PvEa?%}K|inkBY8OnOv} z$$tU{x;TuN82{jlm!bKFugeEEn*=G)9DRMCYZ{W)WY)w;0#)csUjMn)ih98G=kNQry-JAkyIG)%Fgsqj8VD*sqB;?tY!|fd&ob zc7I>)74|pH*C@>~hYu>KM>FGO2n(!B_3dHv<^rh^5qfV<`e2Ycg9U=G9-j1zyw+w+ z4S4JW8kRhYk>wksAbc&L{ z1X7dByI*(cC}UBfIb%;4q%+cZ3ywMWHw4X*LMZ5A$l|~l%hdL}jkDu{u9ki1-szkEX&yKN z_Rd&puHaVM^Q~i==C0%~;)5iTr4MTtDKBR5c~x*YBUZRroBit^<=|74hk!&XiHyZo z@j_VejnzCeONOJ(#~ag_T^aWLk}uz^i^aM8QLT@?6@DyL+N2~Nd3BohJgTxK?)iIU zLVeL!M}hAQhPZf=k(iZ;9ZE3#z&==xEg2pBGhh<8`;tQrjO|gALyiz$H0=q{ial`p zitDo6xVf7$fMcS6ziSWr36#X$*ds-bsU}b0V>IuQzJH+(?c+2#oSBaFY7F-oK54tL zK%;$F9{0v|De3Xu0yIXl;k_Xx*FaG*9h{$$hIpupaHx*^$-gEWfUFvJFKwQj_rA;8 zYg&|N^~=)5voo?>Gni4vS7wge<&EI$t2LI7D|7T3unRWbGu4*M#9s|mk#C?bFR`Jn zu!~=$(}Mr`X%#BQ7qBW)BCMLY%?-FJevAh%xxkNq6{WehmKq$CfrG~{G;Az|7E-7X zL1$_U0?F4W4@0NL5n987D45NW@6TZl$zk)SU$Wz~6&=#6svL2P`Io(mqh>pXU#i7)ZIc^Ry|Uyy&ftp?xG+@I zZ|lJD+YCp_{c6Qwl+ssG&ygaKj@#(t(;P&-!1}!xoqrNhV`p&h=Lw2M|k zrg^zi1joM@Gn@D@;1_DKXA!h3l1}zv7|O{Z<`A1VsWs;Pw)%g!$CT%y{jNwJz$i@7O;j_#}Cb^Oq(Ke%efvAAYr1YYCPR>o>SSuZ~O)JE7Gb>VBOP;!{z%GeOEh{2Cd0R;v zqeL!ANhvQYA|*r8{4HK_9T(fNRx^DR=xQ@LangD~(F{NyI~xpu`?r}U0Lm{AIX30kk*iOkof32Np#m~@9eA^6-!Hrstvu})48u_sVp`MG*I(E^D z_r%$XYz$o##>i+^c}o!aA2VV@dh%Z{fZ%`qKYQO|kSid{1O+mF8x)ukV+)eg)*un9 zP@?r@EWsmG5wDv^-d2t*Q_>*BD50!yLzIdLc9YYIC_zF`;1WpA#Oc*oz!W41of03F znrtu+48&HDK^b9_i`NPqi@`Hx(_&}C9#9mUw3g2oyC5Ur!ZGpV`BMx?Q1StImfIFY zP0JO9eF&DOv6gRT*ErNyiQieCPAnS_O|P`Ii{IO7DaIfO-Fvbt(y&(bVwz@B8d=Ja zk5?!PmZ#H@a7gq=r>eJ4m;2WRt%8m2A6Hg@G}1qAt#AiaRD8r>WQ;uI$rsTqm zY{VMeWGr?UVSh`y0NfPxYLa3c%g z^eX^#E!5Q~5#p`krPOTNwm zX=#CH8R+TH($h1rF#G`v0~0d~GZO5VfraG)7YB%g z>tBKY@6G9300;=^1iYrA00AgL6jUIJ(=I?7nKlDygW1O+7(H4QBt+4Vc{x0kH{ z3a|husK^;WDjFcwpC|=@k_tpE$V#JNeqTr@FdaRo$VOXiQQ{mY%ucsGqH8Il%mM6h zQ91`ASEHn)q@X6({i`emh*D6&oN8Ogd4#oCkvb54N&+yDt5SlfK!7WNAJBoIpA+GA zbd$$Fv(Y?1=u>@4+F7n3V?H^2`>s6l9{Bd)`C{$RpyM67Z;A)fF@*7eYC>$VQe~}U zOF;DiA#olXa-#0|K1|KPcGKRo6__CWQi@1YY~?9{ys2xxG17N88P;F`%n_Eu9 z9#cv6*ASbM&l@ks7l&gSO?cA2YQk|ojg2u9{HK|5BiQXixo?IGEf8x0^R=M=T8RkvIIW9`s-n*bW08Q~C zRd{3IcKnKsXGs-a(B!5Ki*h9tFUnQD4$^?tq+!=ZT%+dq^Nfs8i_q%5-*9M<8_L1g zbFhLr1zbM`wBfc6y-Kqg6JLIl2@CUH87?xlCz|m;K4)44pvB&bin< z8J^MIs#R$6A$*j*z8$rJgegs7@38k+>%JT+hF~;U!SUcnK&>IdbQ@2b=9f-%l!5uX z%5Rm2`olUYzY2C74ui&Qlw}t$M`b@VaH_509eXoGJ`$P#xV~%$xkVYxH2@cO5f>Ae2k+d6!gcKVN729be6zEH;v~>=M!$q?;Pue1~<-C6w zn47oxB?jL-5>I-^gCWYtPx5KvzZ+PEtllWwvirUaYO2#ezo(kCM7mA76!6)O@-yL* zy|j3Y#N0H7sGJg}0U~11O^En@Tm<5tFkHA0Pk8whTN{}|s6`{&)CwXYqu1aG)=Mx2 ze7v!Sm45irP)vQob73h)+Eb!^Ia3 zw?{REb3o&0G7aNA@Ih_R{_^*+E9JmO`Tq%x|TfBklT5 zQotuYdBM0NnzASO?oDX=-G)G$bWRJ@BphKiXKXEMe7UGfuhOEXMA90S1r%iw5ZIqQ z=y%OLtN%l{=81ECW{!8o8s?GfwlQkwC2KIk5$n0)*2*DUU7um-kNET19yN)tV#%gyo<71y-5-_dd=yypt9=x25@9^5NXA9g{cA z91ddF5*bec&EC%L`E!maX~^7{JE;V44*iDZ!=hJ~0UM-TjsewQGpO4b>+V-yws$Na zE?&)vdDjgS2WQMIH+DmUs`}dwBdYYR7d}N@wwmLV7$kc^o>Z#OhuQV7W=4M9xh9Yh zp!pnZmIAwFa;MEq_jeD0$^%vL@@eLmV5nC{dGcA{ z9#BnxI~&yc=7uIwlsGBVcy;6Gvn(=f)TH%#q!hOpqfE$)M-nrrz`Y}R9kKb?JY&``?3pJ)UctV%ftrGNe7ZSwqeU4DAlmUKW@0s?;;=?Bw ziJFMb8nIJ=Il663mVsnE#aD9VdUr&36%b6da7%`RiM`5PO)6zed*Wq^{h;FJ>uz)aMzN2)pJH{9X|O>`R8yIEV>M9v1csY$&L9l z?gD?|dyHGuI^^(M6Lv;@S8D2Gs9{+O?`19Ko$k#?4N{Leg?WWN;;t5^CO?Sfu#C(V z4`_RJ3UE>O@`sPV>2iA3(=uy6sXnjds^l+Q(xN!sDC;UOzff7Drz#dXJhU4;F}xlR z{~0~98G1Iy;VFDqZXb@7*TR#QI7{19Qe&xGRf1#}6cd2KN zUQgv(TOPuUco+6`c-UH;l>?$E+^jei1jgto69bp~Pv(l(pe^wU%@u2kB=7U#IkNdu zxih!>7RP0_Hj14Qwf<1ahKFql#ge#`RdMOzBTf9rb@`8d~@R<(U4H zHkN_yf{_@xwJSI^eR!-E|Kd-BZ{k|lhtCW%@4hbmo_YR#Z@kmW6GPuf(;rTtHU3`PHkCl(Ywy!4cYC`MM(( zw85BJ(@$9}{;~zV3Zrf4{ejM1opPa)cAyeIJX|1&cczcibaR28tl2eM($CF z=F%ILi^1kJ&o&r!#`O5B+3sD(^%p(~b|2Kf{4tpE(IpjLq51bUh#{ZZsD_@yG{;9w z{vJ>B@^y4NWB(IzJ6fI#zUi#J7($&tH9wkm*tn&9O%y{zYY?rCzczlOkJAxHCDkn89=5V?iD2lzO=*~=3md7+-3Rm!=eD=XT zaAU)X*6{ciqwg(Tmu4dE#6_4!Zd1Ydwl0s9EHh^$oF#RrQbs8Rfu3ON@n}S`FJyd@`ZfHU!Q64)X z7i=Wl;8HmFEq@oC6+*&;hEnkx#$HV93{qa zIqDit$6e|2{`8{i*r?&WN=>ziQAG9asYgKh=furv<>9mQOqCvC*%Pv~i0KF5Ec$M!i<<{P$#fhe883Q3~`WxzFgzw|LIZ7&FziXTU?w%bacKNW5Cp~*nRr?M!u`3 z_k3l~!d)bGXn@LI_x1&W%*~%x?Wz4to~xN@Nw;S2+=17z2A4|urh>Ch^oD$5 z)lhpD;Q6mwT|6o}zYj+8(fHT2Q->6N@Lip~Pl_|(KhoyaxN|Wyi*YIbVeGPH$}S>% z$jCNX3WdC?=Y8Ji`S1On&$&PM=UVP_pL4GJ+Ri?neF7kQ+SjxJ5D5S{pI^Y)7QlGb z-w72AfB-N604nDsB0!ID_I7juoIB4cptD&Z8XyOQ!DL`^GBR>1^7EU9lAN59hL)O| zhMJldLi-00T6zWu13fJ>GYbndGZzd74G@TegbYkV`mZN4l8|1ILYOi!gB@6W?(<(ZODV4H8q-+XjXIkFXi3i%7)coc zHQ?8+T*B++E3cx;DKD_7Qwo&>PoJ?cn?#wA$hsgO>565(6zf8f;Bfg}Hznp8+%Gd< zug})H5wB6f^v0t-gek)**}-@wdm-x<^q%D+a`t>}J>%HJ!9EdQ-p zDeQC3W^DSs=F0RJMBbw88Nm2Gqw9wHzfnnbot3EwPE|>!M|OiLlNC8ydGm&ZnAw}# zKVBRqSJ&uOlAMEK{;v0!UFaH@jim|;GIe%HU^yvuzK6DSEJyj2h?ECi;Ri?Td|8K3In208w91JQ44&1x7gTl4+v@?ZXbkz2CLNBs!<;)P?Puv+ z4&52o-D!<;keVFTsfyLNlss0{tF@lBOM0v(z7}^S?kHx>((Cm$G&0t1r<8<355|UM zRHw-D?#6pK=G$by<0N{&9de2Dgrln^TiV-NGG1C+cXoqP-R(=X^cHD!o@C-*bEdc8 z^<~(Ztn|Lvs#Zt2@mRUv{0voW*V4_kx?34v$rpM5hNoC2zjw|V5cjj6P+7!xj-Twu zVP)rEWUr?1q%2}fw=wp+#9H(B#Bk|UMT@lB>Dh{x$Ywj|*Y6AigA4I<&N)X|;yec~ zQe#Sv5BOT>HZ>Nv#DmGUUA=-;th@NFC-V-6%GV2@Uj7JGnY^oSVY6u2g2cmeVP!FF zg9736-)C&MlAuq%YpbZO3UB8%bx+SqnspFH%G;tAkiFB(&4Fwu1!5aBA9Vr-vZBqN zD0vWlZEinbfT?q__ITP$6rBNX7_>Kq#O*8fYgf_@`-9vR@9JLM`6{suBU^35*0GbU z;2jQ!XH4k18+r{_q~8S^gj5u~vEhkm=vShZ-VF*j=u^9pKBB%^g;L)mymH(Ah!U)s zv6|dD5yuu@ACYsE*D0CqYRs$-USZ5zot5w$B3!xgc5Z$M?YtQp>{35^jjld6jKpd? z<#iTAy8%wybln-fel)%xA`L0i_H}oL?P!nMzzX$bDpYbo=0`GfNd|kob78 zk;(-j6vsef2W&*a+JINd^(D^xpZdPVxtZDKzGrxr8k9$fF6oUtp{2Iv(&ZcI^baKE zl&04;{$+&Sm67LoFuq#g+`l&&GNK%xQtMpJA?wY$XazjZMMxr9~V|G5-Fr32HU#O{qk}tOP3ANt(Qlqaj+lgL<5HS@pkMJ zuk9XFyAQ+s%Z|u^8*0+YKaamNjqti){BVIdWI!yBLtA}asc~Tez`BXqin@TiP?=j?K?S z9szP81J5@vy!Z{m=wBshlYnbAv^e%{jrACMTz~YV1ueeMH&V zRv)D1`FTBHO=)gO2%S|Zkju0l-m&R2;&Mxq)5d>V&9Qu*>Rzxel+kz1Ik+ls4cgpW zAed(MutLjIB$JXlit2B4dnxwEDnb>|tDz^sVM4ME^eYR3fEo+8CL_1HyiY<7iTm>s zOnbTbu_rvtKpw~r{4i8XK8E~ad<`Mx$Z;I2;kA5=kuy4`p1+73Gydl8hlwvqlV6KV z=Cb2+{AlT(58;JD?t(IO+$I}LgsB*ec=QiTuP;x^rX4Ki-rzr@XBCq6AMhqPUBDLf%5%t&%?p?#^jMftu~Ew(8p(euRBcPGm@dM{?TYQ&gZ)+F@nY-Qp?+-! zckxw~0>5VI?HlQsU`O+e7mIsZEkNDRb|)l4o9D+RYd@`Y_$FAMG}0KV`9wdqqR%+U zHxw}jRp{;u@1}-Po-DmXJ;GQDV^A^HE&QWX3Q<#}wpt91+Z9-a4_dP$r7L|F%;{Q6 zyituP&DTD$s3h{7UBGXA-y)f8Sf#q`)MYYE!pYl4x}{f>tZI$D*5H%8oE)9*9IU3r z&7&bUP0__xX81)sMq$`APk(UEqI6lF)l;6ORyu6zL}&6k>z^o%V4-B86yDZ|1~aPP zze>%Q4^dW9O6@iA=Svr5;no!{m4HAXR{uKw)PF({5!W*}XN{Ne@>4E(=8Bi^{m3?og@&q;0O_gzRnOt zN(R)z)(;`My7gF4>V8*oDY4wmYg-lzao*Bekz8sB9~1nEiz%XhjMCeg@k3NyO4SGD za83(|ERT{NGA#~hh|$TGABj6xkjlB2zA$gR&6yL=^Ws&fmuBT z3JXjTDHd{7f%e&PKG;qWoigStK>=ObGNG8Yw~PqhW=`~)qG7oYX;HEcRb(J4qQk_^W9h9w^gU7RNUUWJ1*}B z8!*)V%IwC+BwE1@N3`xLPQ~a3ws)ch-->mwF*fw3RFoRM_Mo1_Ah;axEX({PhmsWF3h}P_jE})^*^> z*uQM#pm66|fMr&;?uqWlag)inP!QiY3Oe^AQ6(iKa)+sycr4`qV2`___YN?9sg}B}3OKyT67d;vIiUlbwG!HydR&J-QE^YoK8By4X zbS4Js8?-xfD=&tmthl~&?H?z6`s-ht+W#KB&4%08@z$nNaSEH90rQ4c zwh63f;DRj>X7{@I;STZJ&PCF$VVs-CY)n7zQn%43&BdC@wD+G`g=M-YnU(c74ia9Q z7L}hs)Q+13=B?vO;_8t#pwjN7V=)Mdi|b8$l3O$C(F3T*Cy+o*OQ8E3avwy_B}N)o zxvgOCp;>uSO}&7k>CC(?SLwM{SKpktL#W(-N!EE*g@shGLnGa`P9GciY>7;ar^fOLB6?wMF&h#3JZ*vKrzI z3sI$;^Ak7WzE@-7YN&ihf$eMHpHU-rHdBqsBFOn{{qt0m>b;5vY?*0UE>_uXIi$aR zuq-jZEm%nRZpz@E&AvEKUsJ>9)F0N!ofYIm0hPwyce8z!PgB20OE?jNvZEmK?Cd0D zx;5H3uC`oUzMOx{AdeoIjsKeLqE#pi@QL`{daA;yF zdBX+*^7%#aEoXDZM3N+u9O|^^`Q?nxUzZx$b`30PY@05D`WmqAb|U{Aw;!eKQHcty zI9pekP2vq(QC8dvMJd_uynP2h1bdC@ThD{*Kb(iHu1a8N6;a-FXR-~3w;S1q7WGSH zoM`ol=5q#<9lL?}(5!&TmngE+Pib#hOO7we%Q`%yIdU^GX5x+d(>_Ghz*cAEuV=5O#BhVZPoOy418a-J` z!@p0_h3(EuLJQ;ye0^c&$2g62rggng_}12z#hmNl!Aw|2xY3VaJnN_>g)6G$7?Q~9 zwW>^0bGSosjI<97dE4qVwZ%bn*B174u`{M=;(hx1r{)%{kn2T3ucY@$$zTX{teAz* z#O+Ei>m%_9_SmI?`d+{R)KKC92xZ97tC(2+s80Ll*8pZqX~>BZt;s1`Yi`LQm`n63 z>3G;cVfMI4zIT6H9p@w2^FL-*^rw!dn%Qps5`fJ8;%--2gw?Sk)3KsqQ@Ny4AH$>` zh!8Lni-K7|U=>1<^s*{4>Oh*h?38gW__}{IiMo2Zdif?oM#%)(LhW}N$6wCM?N*<| z-C8k^6(75nOI?VO@*_i2j$$#_{E z`pP%9NAfDZL(-0zI<~`r1R1H)A{jp)L{)GCvg0!R0w^bQdqrJKnY;m!@!TskXeCjL zE2VF52cLl16-<6Z7UQ!_gr%UF4J0H`R-`&m;wYVo%|2-o%AHbB=DOU27D^>*pZWJD zc3zI;j$g0>JI3THbiLa%*Dg@VK1V7e-+sjAd^#i3VxTpD0BDG z%`D4~;;4*&+|W9$^>oT6hLh;=+2DX#EoOaaHzA?5RS#N_53xVhgo~~^usolk20XZo z@|}70XkUlq4jQUOcdyS)sW93*n?0!YAOG;BC6?Qka=6CT;kaWqRu8`HYt#3#cDu?$ zwaoqfvT>>3A*;N}+O{`zBY!jTTMlvtPICl~UB_*n# zJRTfX{IsN3R*;Fl{L(4GTX({<&ZK-4gH;RhDtf)0F$ZngGt zTNgryZVOs;*kZZV;3Ra}_aaeY_CnT|;3Z_b<7Nm+fdoqbMad&;nDc2@J-twD_^6m0 z5z5zT{BU>8r}1&G%{cg;Mn0k7(5_rbpNwk737!g9p_c9 zS~GeLb!`)ga0xo-R7o-mPuSmnTl(KT{`{v>Ui|A8z-oXC%aW zK&?7lgva*OX;*!3WRsG|cHV(>ptYJW*!6qz8`5*`?^!g1#IKk4R~IKCDs?}*DKaB& zs_zkh3!k0~EB{MGlK3(z?LR%i4Xj)}59`$X8s+78bKeVHXu2W62*LQCK@g54vU%J?DPFH1rS6W4k?SGdcXP+Vy*L8|r1RFTYOj8|76j z7TCkIgi{~Xp8*1`RYy%kXrJCe-@X7e^u^}48+ZD}To#RP`!s+2sD)m$g(a_3Z&eft zWw=Qm-x!a}{+VQ%m0Vih&^M|$rF;!q8>tkV%xuKP?zK?xE<5Z?8|yiz~IHsCn?k^x}CFLf?bR{V6Zv zeFQ47)J_;y_LGK+g3=LV1|8Wtr+dBV$72sW6?Vpg7cz0C(<&-9Z=HmlobF8KCI|QW zgSjtTE^Aujwk84%0m1xl^JUav#?Mk+SnL>wLL80f>gNj~I#z{u0$Af`?x1!HG$g-GQecOBm6Kwke^HDGT>jIc{PsQnjsLDH-L3roRF`}Ke&6|`nIa>jAf)#{ndni$?-cVz z1~Dm|#$5oN2dkpC|FnC=w{ZJky}tH)uRnOIz9s}QJo#l*KM;%msk^8~F|3A}FhANp z19H?Omhe%OD#p9(Ti57Q4o?e1AV1Y4&j80CuYVYC1n^4#s1aC=c(QP9o|p3%ZP1BY z<8Z_$1)t-`3=B;nr@Gt}#A78BhTW+*rXS*eVV$)y4eVqar<2{8N#Q~1|( zA?1#{Kitm%-q*i$E7rM`6wUyi{-(6%(|MoMs&mg%|GKO&_f-6|+UJAN`3z+E8E`7y J^Z9K0e*otf71#g( diff --git a/src/Umbraco.Web.UI/umbraco_client/Installer/images/loader.gif b/src/Umbraco.Web.UI/umbraco_client/Installer/images/loader.gif deleted file mode 100644 index ccc9fd6b6fdfbc10248b51f5702ac8be8028a878..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2113 zcmV-H2)_46Nk%w1VRHc(0OkMyxnN6?Pd$)OJg!_vo>oGwJ|U!7LzzA#lu7HXE^AN32{&o>fAtT11&tK$1{CpjASaQ9qPWJ(^QLl~O*o zUP-fDN19YWxnD}MUPzKoI-^)a|JI!E#fxR6(#?N4Qo#$-JtkSw+87Hs`~UyJ1VSUPj5ZpR-*^tyx5?T1Aa9 z7Oq}MoK--PG8daQ8t<)Z|Fv`fty1rtO7EOP|GjhXtz-YCO8@`=A^8LV00000EC2ui z0CNEt000O7fCV8k2wY?&0RbpvTnGUxWMohQDLY~)i6~-?j}~H+2s;)E4*?fsT`8Rw zLIDUwqX7~m5nYZe5f)UgunLJ(5oD@Cxk3^uJ0e|~A{MGCxfcN{TxLYFLKYE|DP$su zVAVtq64? z58{{vBY=Pl5h)}<5z&H#1TZ^VkRj)!g$pARf{4iAWFi?a793%L;pItyBX7JYkRZWI z3kG2SW_j^aFUSV~*T7VOA;Mo3vQH*51@hp86#xTL2#}J%%LOn#CIGM^z%YO}KEN<$ zg9Y*jHgOPuS-^&aj}v78Bp|WG$N&Io1b|V(#)}RKWfA}&u;hdYHVsZlKyjvk91lX! ztf|4gd2=Q-qD!Any?S&l&a-RZUIV)I@7A$%AD`Vj`19Avt7mQ=J$Ui!$F~QcE@hk z!bzl@bmnR2i+JWa=bjX5`Q@O57P_Sq6}Sl~ihcUo=AtkSDk^_HDDY^fMQ(a3s!OWMW~QmSiYcqD%6cZMpN^{Qu7pZ~z^k-QXsN2ORygdY z#nx);o55nZESbgT%Imb$>R7=8bv9e&v&=So?4`*H%Wb&MKAWtx)uyZNe*^r0DIV~~ zEAPDY!uzeHHq@)HzV|ws@4f)X+pnMh1`P1PdK$bhy6ZOV@E;EBaHhfsk4y3Yz6eix zF~1gXOmD`2dOR<~50^}>2@=04^1L7`y0W}2Kl-x39nT!|t24t(vdKJ;YQhnE+>po6 zL>F!J(L>*$$I?tU?ex=23yt*DNGlEX)lf@K_0~ya?RC>yb4|6?Uwa+4*j4Y`^V)@4 zfxy{l8%=iESHm4Q(R0)NwB2{-k+9U`s%Fv(Yf9?v@W~suhSj-?6{|1d)c?=j{ELk z_wGCGOp8wX@dXl)#PGyN?zHgCuMWJ~(Kqir)6iFs`t(?5ul?M{BQHMxfIkqRz3zLY zum1Y%x1YZE)!*R${PV*fz5Mj&ufF}v>a+1|V9njOttC0@Fx8J)Vz`^YbGL z@t8hA1~GqbeBelFn|t7nS>pXpa(}VzycI7ODF7r2rmdi6?VXbDr_N_tPBGJe8~eL z=%5LvG^P#|umb}eAq-ex00T&%0}?P`1Xn;n4|FNZEi?fLJa~Z)EV%#zu%HJsh{FtY zU<48@paBF}Cl44k!1wYsU1BQTvAuwP9K!5-Ql%Ro;LZG1hG=WJcumKa8KnWBWX*acb1ql3L z0UA((O@W|+1^nQp4A4Lke1K660Ko(hP(VwEpo1weU;sUyHEB+Na8nA5fB-(A!w3ej zR1@$Zpgv$h5n?a_cm8w&S@0)7c>vWJOn?Fe=)n)J3Iw@MU8_|CxaevZ@oIbPqFI(=X2^&g1V2EAXK{C;ir_pRCgw-*23Tm1fL z&-bHKzMq@&|JodeQ7{?;!z%=IK;8l61qP1i4B{Lz8XFp$n>mEFVk#yu9&8uj_n1?` z(Ad;1VVrfRLXh!jzk+kr6OT!Yk5ACxmx$O|lyY*qY~-mqp25q!XQ&EZxfA(W&2O?z z^QJF9gBSZPbnFy6S|Re%kA-uQ*IciySyz_%Wk=oJC7OM0X28y_y}!O@HZf&fUKM+~ z%%Nd>!Q)eNf0r@b-d(}HW9jMX>+bGpxcKg^_l|Y97+D0g{mxWud~|36yF%KX9h)8> zo51a@c3|e`r{{YaJLG(KZOObi#kM@|Y}M9=%Nr86&-0!A`Spz*?$__Vwc4J4??C?V TK3#?%c@Gc!i$9kYV6X-NC#aWq diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/marker.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/marker.gif deleted file mode 100644 index 03c48f4109b7cea9a8ce05b6db776720fe34f89f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61 zcmZ?wbhEHb)L>v`XkcJa$tnN;|G(l-7DfgJMg|=QAOOiQFp0SItvubEFlP>rRQKk4 OHkbdqKDZ&mU=0BF2omrB diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/marker_rtl.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/marker_rtl.gif deleted file mode 100644 index 36ab05d481c0f7d82bdf6b3a8ab415d9f14e26e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62 zcmZ?wbhEHb)L>v`XkcJa$tnN;|G(l-7DfgJMg|=QAOOiQFp2i`uRP7ptIWLRR`=#* O3s3zod~gFOVGRHTl@j;> diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/plus.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/plus.gif deleted file mode 100644 index fcfe0f53fc3d5f44e692e6a71f24f64af700e7ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52 zcmZ?wbhEHb6%&Is E07=ITf&c&j diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/remove.png b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/remove.png deleted file mode 100644 index 083d3f3164e338b0b2842adee70415365ba8ef0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 720 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 diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/f.png b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/f.png deleted file mode 100644 index 832559dcca7220ca7264ea5e7111a564caf76008..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3237 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 XgwVZ$8ZqcX00000NkvXXu0mjf9_l=V diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fminus.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fminus.gif deleted file mode 100644 index 06518742e34e823ba74e45e629a3896c284d7969..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60 zcmZ?wbhEHbWMg1sn8?5o85yajrlzW@`v3ob#h)yU3=GT+It)Mnl4oGzog-_mC_X`~ Mp@^+4pOL{D0FYb^e*gdg diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fminus_rtl.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fminus_rtl.gif deleted file mode 100644 index 3f223fa38a168b8601232f5a5e6b3c8076935ea0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59 zcmZ?wbhEHbWMg1sn8?5o85yajrlzW@`v3ob#h)yU3=GT+It)Mnl4oGzIa8M;vElNa LPNN$OxEQPfo(vB% diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fplus.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fplus.gif deleted file mode 100644 index 39c5b369b832ce464a46bbf4a53190dc4969b8ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 108 zcmZ?wbhEHbWMklD*v!E2=g*(tzkk1a_3Guzm(QL(TfThx(xpq6ELk#t{`_gvrcIqX zwWX!y|Ns9CFrfI8g^_`Qhd~D<05XGtMe@Q1OW%cOI9z-MyjU5IOse%XEx24*Dk03s GU=0A((JKJ} diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fplus_rtl.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/fplus_rtl.gif deleted file mode 100644 index a15ed542fd6a61ad729182ed7b8533687cf7c931..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 108 zcmZ?wbhEHbWMklD*v!E2=g*(tzkk1a_3Guzm(QL(TfThx(xpq6ELk#t{`_gvrcIqX zwWX!y|Ns9CFrfI8g^_`Qhd~D<05XGtMRI~?rkZxB(!{A8SDhFfrqxI`^SmsL=n-UO Gum%9biYko& diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/lastli.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/lastli.gif deleted file mode 100644 index bfd45a04cbb5027e96648e808a12f89d048f117a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55 zcmZ?wbhEHbF2lFH^( HRt9STW-JaM diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/lastli_rtl.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/lastli_rtl.gif deleted file mode 100644 index c34cdfe874692cd78704896723ba5c33b10fc6b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54 zcmZ?wbhEHbEOD^brBh!<0s&%av GgEatCm<_%F diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/li.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/Themes/umbraco/li.gif deleted file mode 100644 index b0839e18d0a0bc2eb4d896c4457c8578edf7bae8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47 zcmZ?wbhEHbWM^PzXkcKFm6iVg|G(l-7DfgJMg|=QAOOiQFtPRI3#_VOWv~VS@^=YP diff --git a/src/Umbraco.Web.UI/umbraco_client/Tree/sprites_ie6.gif b/src/Umbraco.Web.UI/umbraco_client/Tree/sprites_ie6.gif deleted file mode 100644 index 782631ddc6af183f4ccbd2e287d4e57cf3f25833..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7307 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 EKd1buAOHXW diff --git a/src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_background.png b/src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_background.png deleted file mode 100644 index cf55ffdd68ed42f2d70bd7ec2010cee86c110816..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1916 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 P00000NkvXXu0mjfA`AaT diff --git a/src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_hsb_h.png b/src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_hsb_h.png deleted file mode 100644 index a217e9218e6a512b507a35e8a6141f0e56193439..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 970 zcmV;*12z1KP)h+v*8l*BQUCyPMF0TmW&i*=2LJ#e`2#0~ zRsaA4P)S5VRCwC#m|aK|K^(yUyEA+4?d5?fXd;=M7I&7CAQ1#X&_j>CL_YN-gn@+F zgHSIeAw>3IK|;|7L=oge5IzJ#A^0E&>G?(8=|!HGo*#L7AKu<>4|8pMd)HkZt+#{w zXC7weW^aBB|Jj-SF*9F$o1tx>E$qcMycycAC@-ZBJ@9=M1Art+Kw)425XHpaLf3nT z*o$qs(c_|+-@n%!JfIWz{MV(NRK`u1oK!3sq$xeUk30*0rTO&r;Lz}xX3Q?2XDLZ^ zNtQLm=b0`ovGcrzGy{tZJ8^M;iCsNLZ*=us&BvV@!eqi&`J(hE1%Vf=JRxLR*23X^ zHtTjcstkxmW4fX%R1Gc%0Gw!YjZb;hvts~YStk9-f%l(m0w0fy0I;#P!Y~ZOFd0Yc z6v|xg8E^fO8d9H}oln`OnfJJ7mbVJzw2DT;EXx*f%c_Y}(B({td#$~-(d7^m2>@v8 zyhBz%)<4CSQ06-3bdbu#v?m=s^TUgQAW80>SVT-DWO+Bqe^ZT5c^X^}GWW!^H|6DM z{2&`t&HcxOX}{XxtgBuQu9KfyE}VDPRTnB@)bzR0U55`X2iGIf82PEXs$7=k;M$J@ zqW{A-4Q++h&2S`|@upE=bVXp?Ee;`O5dSyi! z_6LF|n~ze5J~HYyjR0NfpC{*ZC2Op&)zJu?In|^S_t5Z|X#`9oU=$;ucVssLNp#Z) zm`1=f0;Ul#jllmj0z<=NR2qTWnkv%>>_H8 zN}^l&B27_@0BI^lKr=??RZm8DS1SN?ceNr_7}^MstN?&p_nzJCxJ2r=d!JKa1US_; z?df}&-f3JTfPWoH*RQmY%H4;rv~L($))vJGczsJ=-;&x0XvXM>ju9#M``?gN>ukHI zbY@GK!e7T#7lFRVZvo)JlL2+7ag6|%dL;F~c&F(xt`Sh)F!UJ#{5Lm80~&$3`EU8J sdf~SzMzo^mJOqtv1hQW_1cCoK0IhW09jn*o82|tP07*qoM6N<$g4YtalK=n! diff --git a/src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_hsb_s.png b/src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_hsb_s.png deleted file mode 100644 index 7826b415077be23ed1b1bf05b2da62d4aa5b1c67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1168 zcmV;B1aJF^P)h+v*8l*BQUCyPMF0TmW&i*=2LJ#e`2#0~ zRsaA57D+@wRCwC#nQcf@Q5eUc-Fr6Onu3&AF^rild$UyX1q~x8l(L`*3O@uvFO(o; z_5wxr@*$1tgY{vMl7$6QeMqAy6e1xqkX2R|TUIxmtGu(iIrnyNAC8Ud=Dmt?_1xOS z`EYjbGoJs>o;~L|ziI4tJI8S#VLo05L|S~@48(C$eTIg-ib~hV#^6c%;`soO zlezfi8;3YL0sx8%PiTw9QdL!zm6bIwEpOSPl&u?AQj2Q*-QCgA(fXtl0Cw!$vnn?|%wceFNYM&oFvQ9@3=R%a03eJUpBbjP zxp`q)T3K1y!+W<%Hg9jJw`vg*Mw-&Q9tyNHEd`IT=#UEpqjD~4E z^RfC@Z$F3^H8z_~ryHfSWm(qgbTa*XVcv?&bj$btfnHa?*um@dDVpmsF>?L3*Xu+0 zyNG<2kdR<9neuc7gy{DU#-ng495vU+K24>ZIF9oRKKZ>NR5}3gyyPRArjJ2 zf+9GneqF87jYgx)wVGJz@o_T{>vs3KTv8sMfSye0b7#*&eJ)p4MuzH$z|-elQ6iu& zBJkq1T~TGrm!v941lAN}Dx|xuy)#M#qC|icBA~R%6M*&npp9OT#pigC=nPp5g7fdQCeDBTwIKaKxO%EOa#U(w2IzM!^(@?E$JU80&VS` zsw4ubW>b_1{D(wfc2d$OhEe0SngVs&z)y})BJlKi*ZoJ2WgLpv7LrQ@Xd0=N2vB3; zSaOMgR;yDh5zy*l)Jg<&+6m=42}FRVDYX&-MAM;h_SjzWOGMK|6M@hlJ|_?D1b{O~ zcb%xIR|gRYCCsVwSC1dqf#;_$G^l_GXhlEMv$c(piwF||u`vwu!e-l0Qj(pWefUUx zZQbpLdh413tIzMB(#G&_!c^|xjwk0X-;976L=pjnXgn@jo3O}(`?iG^i6#O_)CkmG zxeWl9>h6da$t42Xkw=pC+TE}fa*4p0M-sV20F9?}B#;Q$d%jF+*QfVdjmwBCrAMCe isv!dMU5CLC`)2?-k0%YdqHBTx0000b%7 diff --git a/src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_rgb_b.png b/src/Umbraco.Web.UI/umbraco_client/colorpicker/images/custom_rgb_b.png deleted file mode 100644 index 80764e5d6dd8aac3c5ef87d83b45d29780af1fe9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1008 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 T=Ar9_QX-DN@01`G*?mYE;P(j>ieaLJ zp1@KmP7DbnxHd5epSMZF>_Ios1X7kEzM%)`8u}mxVv)EwgAVwi+aL%gVHgVE_pQ>@ zbuG%W6jfCb)Fr3i6gRMr(1H#Un@BbTwH6Q2xnfI1$74|x z`a$b3REt~>BR!+Rjm(B~!)%yB$32-1&(IZ{4HWmQa;H~;maQBTMsJ;5 wu=L25Ib=s<$1=nh_wOs&FkN?ph+v*8l*BQUCyPMF0TmW&i*=2LJ#e`2#0~ zRsaA2j7da6RCwC#n88ZJKomvKOQ$N-q%EcB2MDDFapS&Ut2tcoJKO0Z5XR@gxT5=E@BPRC5JM(-{EmRs+wi z*>P|@#ScQvT9QAwUwvX=#Ku^(QHKpv2~+}=KqXKKR03@~kh1h#z3lU2-9S7%K9wHY zZlHd@zd1k|%qmdMx=&^ql(G#p7!3Z$L#9{gzl=qFW|hX6M^<+t`|yc1Y?-gyyEotInm#MOm!f^^y*-dYoVJ8o0wy4TmX zR?7dfn;bc9{iI8VpES5yofZaYh;TjCpK_UhilywQPp{V&o!)zNr-EuAP#7%xRC{Am z6GR!1|0e8}W%vqnuQi`zyC%VuOt~~QLec?j5=hH8t&PuqF6Zm0mDYFhgqkAab@NoR n(3#>M@3IhYo&TccjBE_r(UXjnZhw#mdW^x-)z4*}Q$iB}AxU+4 diff --git a/src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/active-bg.gif b/src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/active-bg.gif deleted file mode 100644 index d608c54698e081a3deae0f1b9b827484deaeacce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89 zcmZ?wbhEHb3xI7cXAAbm`*d3l}e6x_tTajT<-q|NpP}lZBCift^8z p0SG{98JMLwcHQ}C;N`q}uj=cwv(saC#4b@4SZ=W5%`PM diff --git a/src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/hover-bg.gif b/src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/hover-bg.gif deleted file mode 100644 index fbf94fc2c1891ff891f2d3c3208cf431c35a4628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89 zcmZ?wbhEHb-E$->CMz|NrfPt>_6 zHL)Z$MWH;iBts!2BUQoO(>H)Y2dIbvWC#PZ6vwVR9}T>mSMODQeRg(w%#PS4t~~0B LE8grwc6hbmm72G|20~mA|fB */ padding: 1px 3px 1px 1px; diff --git a/src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/title-bg.gif b/src/Umbraco.Web.UI/umbraco_client/datepicker/aqua/title-bg.gif deleted file mode 100644 index 6a541b3bc1e4bbee3268e8a5b431e2c7e067b3b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116 zcmZ?wbhEHbWMmLyc+AI8Sy_4e_U#7`9z1*Y?D_NOFJ8QO`}Xa-ckkZ6fB*UO=PzHr zeEwc6hbmm72G|20~mA|fBtHB8VX&K*sP!FEoV37-@-w z0R<@%0vdJ3g^a|I?Pj7#(uKh`L|Finie4=6J7+yFJ!?;2&hK|#eqYx=!aJTkdGdK~ zpXAvT*|;uzpq}wF^XC|f7Hy%(3T3cCne7xg0NE&VQse^UpvX;;2at=R3KMyuTq^;2 zC|Y8oD$05(`vPTsz*a#S0tVY?u&+|C9=JNny<&2Wlxxu0Hl1B<&bOHJt%U0!T(_BC zgSodcw-fkw;8z2`7Wg+!zQf>qKnNTBb`ZKuVKa!`hOo;JcIVTBAiY~i59ZSeL+aC| z!@BeVWcDfiw+cT5^1&QG406AoIcj8%=$U~We@)9obve2q{HDt%A^V9eOz8`!W#PUo z{Glyegxu${_yBUTjF`%Z|48DTBtDfyM6y>wiIePzB&H=%lq8Adt`lWUQ^v6JD^_ko zVMxpW0>xpRzl-zJT46#fEanU@Jl2Z$aN#etkRrv0xcG#WQfd*YMOH0xfG5?`f?CQG zRZvSM70ars5}YAe!5FKgv{(qnpg$pmfIbJN473Ck12uqBc&`6pfd*o?R4~A_FpDaG zf^k#kwtL0q(ctCQJ>$_C|68Xo9Y5B%<>HN|rPXUc)#iLZ4#(@;I^sWF!BdIIrn4`- z_ReJJ%ebfY&*2}GiP+Mnwl{8s9^RRq>F%t|b=gjA`^L{W-2=&FGRjoj?dL}ulChUN ztGti>UrgEp&DJF~TgO`%XJ0rw;D5cjZkdINhI+^4JxpY(BDmw(Y^2$Dw;%8S`6J8C z+7+J0mD495S)75uciVQyzK?~28yvw)3bIF5-kaz;!0dbQmF@mI-kmt>7+F`d+N zt#PY&(k@T!8mH}j>(;M_TI-IV)AU=8{=a)`lc$<@jm#z)SE${(>{g=Z!tz!BW;gbH Q9Q54|91Yd$>cA zcV=g2cV--{@!&!I-n?WE@8x~(dmofif^qy4@G8*nANW69HN1QADhz`>%YkX+TxJ<9 zm5Knu7)O(3i>EROBfY5Ev-SI%4-rn-7h2V=x)3l+X)3rxM%PbL4g{DZeh#FeoBB78 zJZsDYAdmzo(H85~0;V#BGFC5}eYIkW4~sg@Do%p0+pignoa%Z3L)OEt*Vd46N5P%5&vU1YiFIc&XC+Sz9t9jhOM zb0@lYz4Cl(h+;=7mDI$6mkLA(c?``?=dKE8wWW7=~fmZm*7iDH~&;HXCoWVb$S8r-H$}PnPUb@2n;h_t&L|9^T#^yxcpn z>ov!V1kVpn>ifrE2kiy-66Y-G&NSY<-F>j8D+3{7qYn20j4ROwMeAG+C7NAodVhUCC9?k3)z zo&)fFqoJ`c8uhYKCOx}C7$3Rhcu+mPA}0f58iR=&y?beW{q7>+ZAJSr@$=-*D2Ni~ zuG5^*kd^WE1-@PI@dGtF-_lQ4HxuVX2LQ?eQ9_E-G(t=Uwox;UMUW4=hWalA-M6~s z^v+MSUU@ldaqze$aKVEF%-6~O6MgQH2xBj`l<}<4o379G3}zu1KCA{N-wxo6#*2vL z`Dl?wF-|GC86_IBoy#9uR?O?i`3xh-6=kz!BsXPNP`|TB3f{J>IJc$?+;1Dd>4&WQ znG?vSN#Cwcr!^s#TSAul49*M$+*?>+I~}&BsI8 z4CSW1U^$%3!zkt(A97`5kc}s0A>}Dhxt+EsDXk TX}TB=00000NkvXXu0mjf-(wsi diff --git a/src/Umbraco.Web.UI/umbraco_client/propertypane/images/proppane_bg.png b/src/Umbraco.Web.UI/umbraco_client/propertypane/images/proppane_bg.png deleted file mode 100644 index 116511a5caab86828cdb4e970aebd464b68184fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 554 zcmV+_0@eMAP)Wfe00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUy=TJ;kMXRf; z`}_O;{{H>_{rLF!t*x!Eudlhex$*Jwva+(QtgOz?&d|`%)6>)4-QCvK*4o+9?9@9*>T^Y!)h_xJbq_V%;0v)kL-xVX6R@bLNh`L3?6`uh6({QS+$ z&Bw>b&(F`iyu9Jz;kC83ySux=!NJAF#mmdf(b3WD?CiZbW=H}+^?(XvP^1#5r zwzjsnx3|~V*Yx!Cy1Kfsu&~wD)xN&I$jHdb%F5v2;Ogq?!otG8zrVx7!?d)t=;-Lw z)YQbp#M#-|v9Yn=-{0ls<^TWx!n_`N0002VNklbsh*cRLhs)Aj?jvy^O8-kV~Aw1iH sl<-Un&tDN485tQF85tS*?@xdM0QB5^B4OrgApigX07*qoM6N<$g34Pl;Q#;t diff --git a/src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/arrawBack.gif b/src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/arrawBack.gif deleted file mode 100644 index 9d3f0ca6869d1af01a28961dd171ec57bc826569..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 834 zcmZ?wbhEHbWM>dz_|C)t1poj4hm#=wC>RZa5fcK6KUo+V7?>DzKt2ZL2?h>%238Ik ij|~eBHggCo{R#gdf4GrP-f0R);KHNb68dZ`4AubXbQK2x diff --git a/src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/arrowForward.gif b/src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/arrowForward.gif deleted file mode 100644 index bf3f49e02463fa56ac1e3bf90a7fa8cb14e575de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 834 zcmZ?wbhEHbWM>dz_|C)t1poj4hm#=wC>RZa5fcK6KUo+V7?>DzKt2ZL2?h>%238Ik ij|~eBHggC|{nU;fa}+&IBQ df``db!9XI#sGq%6X<~@hWY2{S2U{2ztO2IrWT*fD diff --git a/src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/button/buttonbg.gif b/src/Umbraco.Web.UI/umbraco_client/scrollingmenu/images/button/buttonbg.gif deleted file mode 100644 index 6268a1bc0d8b6ae8fc753bf2b5f46e5e9106ef81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273 zcmZ?wbhEHb6k`x)IKsg2|K9Tdm#6)IvHkz+egAJQ`v3OO|My4#KV19&!o>eqXa4_k z=Ku9M|L-jM|LMg4him@7-2MN?{Qu9k{J%K)|C0^>AFTHF&S$_26o0aS<#j+L$W8{< zz6902l*}%#RqJ%L%?fnRZ_c<=vF>%!!~X{o0!%cd`aPDsIGAHK-+y9)*V?GHi#p0q zzb$i}yrIHJt()(~ma4sCmiIpFh|ybg#j0_B73_2hs zAUhdY`xF%VQZl=OSFO|04l2+&zd7P=#k$u?PyQcB2vE_K>i1Z3;$V)|eE*BdUTdS) zDs_~dep}|+wV}dCty}EHma4sChW9?~h|$Zs@%~3m+>{r;|2H%!vA49gwYMvAbocc3 z^(nDUnmlFdR3(;~vu4kkqr|#k;iAQhm6(^WSh;GI64Sc%8#ZoKV%)lI`;HyzyLT%x GSOWm&o_o*$ From 8664d3cd3565f3f5e7d1b60e1631e6036bda9772 Mon Sep 17 00:00:00 2001 From: Shane Marsden Date: Thu, 3 Nov 2016 17:03:16 +0000 Subject: [PATCH 015/714] forgot to add file to commit --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 220 ----------------------- 1 file changed, 220 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 6f2728a241..06b339ed47 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -624,14 +624,6 @@ - - - - - - - - @@ -744,7 +736,6 @@ - @@ -778,9 +769,6 @@ - - - @@ -789,8 +777,6 @@ - - @@ -1471,53 +1457,35 @@ - - - - - - - - - - - - - - - - - - @@ -1525,18 +1493,6 @@ - - - - - - - - - - - - @@ -1566,14 +1522,11 @@ - - - @@ -1590,19 +1543,6 @@ - - - - - - - - - - - - - @@ -1663,7 +1603,6 @@ - @@ -1674,7 +1613,6 @@ - @@ -1713,25 +1651,9 @@ - - - - - - - - - - - - - - - - @@ -1744,15 +1666,12 @@ - - - @@ -1771,35 +1690,14 @@ - - - - - - - - - - - - - - - - - - - - - @@ -1884,12 +1782,6 @@ Designer - - - - - - @@ -2003,11 +1895,9 @@ - - @@ -2020,8 +1910,6 @@ UserControl - - @@ -2031,32 +1919,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - @@ -2134,100 +1998,35 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -2293,18 +2092,7 @@ - - - - - - - - - - - @@ -2325,13 +2113,7 @@ - - - - - - @@ -2383,8 +2165,6 @@ - - From f266c7891563e93b30f6442bd69eb65465055ce4 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Mon, 5 Dec 2016 22:26:09 +0100 Subject: [PATCH 016/714] Fixed issues with user creation and deletion --- .../AddUserGroupTables.cs | 13 ++++-- .../Repositories/UserGroupRepository.cs | 1 + .../Repositories/UserRepository.cs | 4 ++ src/Umbraco.Core/Services/UserService.cs | 2 +- .../umbraco/config/create/UI.xml | 8 ---- src/Umbraco.Web/Umbraco.Web.csproj | 1 + .../umbraco/users/EditUser.aspx.cs | 2 - .../umbraco/users/UserGroupTasks.cs | 42 +++++++++++++++++++ 8 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupTasks.cs diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs index 603517bbe3..ea566c0238 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.SqlSyntax; @@ -16,11 +17,12 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero public override void Up() { var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); AddNewTables(tables); MigrateUserPermissions(); MigrateUserTypesToGroups(); - DeleteOldTables(tables); + DeleteOldTables(tables, constraints); } private void AddNewTables(string[] tables) @@ -162,7 +164,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero )"); } - private void DeleteOldTables(string[] tables) + private void DeleteOldTables(string[] tables, Tuple[] constraints) { if (tables.InvariantContains("umbracoUser2App")) { @@ -176,6 +178,11 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero if (tables.InvariantContains("umbracoUserType") && tables.InvariantContains("umbracoUser")) { + if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_umbracoUser_umbracoUserType_id"))) + { + Delete.ForeignKey("FK_umbracoUser_umbracoUserType_id").OnTable("umbracoUser"); + } + Delete.Column("userType").FromTable("umbracoUser"); Delete.Table("umbracoUserType"); } diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index b13033c7b3..d214c0d555 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -194,6 +194,7 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM umbracoUser2UserGroup WHERE userGroupId = @Id", "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @Id", "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @Id", "DELETE FROM umbracoUserGroup WHERE id = @Id" diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index cad213d1a0..1bc86c1683 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -284,6 +284,10 @@ namespace Umbraco.Core.Persistence.Repositories /// An enumerable list of public IEnumerable GetGroupsForUser(int userId) { + var tables = SqlSyntax.GetTablesInSchema(ApplicationContext.Current.DatabaseContext.Database).ToArray(); + if (tables.InvariantContains("umbracoUserGroup") == false) + return new List(); + var sql = new Sql(); sql.Select("*") .From() diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 81a71e0f05..9499a142d0 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -321,7 +321,7 @@ namespace Umbraco.Core.Services public string GetDefaultMemberType() { // User types now being removed, there is no default user type to return - throw new NotImplementedException(); + return "None"; } ///

diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml index ba3df007e7..3edb5fcedc 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml @@ -213,14 +213,6 @@ - -
User Types
- /create/simple.ascx - - - - -
User Groups
/create/simple.ascx diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c2b5665f4c..411657f5fc 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -429,6 +429,7 @@ ASPXCodeBehind + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index 83c30ddc25..6d7c4056fd 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -39,7 +39,6 @@ namespace umbraco.cms.presentation.user protected TextBox lname = new TextBox(); protected PlaceHolder passw = new PlaceHolder(); protected TextBox email = new TextBox(); - protected DropDownList userType = new DropDownList(); protected DropDownList userLanguage = new DropDownList(); protected CheckBox NoConsole = new CheckBox(); protected CheckBox Disabled = new CheckBox(); @@ -162,7 +161,6 @@ namespace umbraco.cms.presentation.user pp.addProperty(ui.Text("user", "loginname", UmbracoUser), lname); pp.addProperty(ui.Text("user", "password", UmbracoUser), passw); pp.addProperty(ui.Text("email", UmbracoUser), email); - pp.addProperty(ui.Text("user", "usertype", UmbracoUser), userType); pp.addProperty(ui.Text("user", "language", UmbracoUser), userLanguage); // Media / content root nodes diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupTasks.cs new file mode 100644 index 0000000000..0aae75d278 --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupTasks.cs @@ -0,0 +1,42 @@ +using Umbraco.Web.UI; +using umbraco.BusinessLogic; + +namespace umbraco.cms.presentation.user +{ + public class UserGroupTasks : LegacyDialogTask + { + public override bool PerformSave() + { + try + { + var u = UserGroup.MakeNew(Alias, "", Alias); + _returnUrl = string.Format("users/EditUserGroup.aspx?id={0}", u.Id); + return true; + } + catch + { + return false; + } + } + + public override bool PerformDelete() + { + var userGroup = UserGroup.GetUserGroup(ParentID); + if (userGroup == null) + return false; + userGroup.Delete(); + return true; + } + + private string _returnUrl = ""; + public override string ReturnUrl + { + get { return _returnUrl; } + } + + public override string AssignedApp + { + get { return DefaultApps.users.ToString(); } + } + } +} \ No newline at end of file From ece73578c86525a217783bd715e6333dc84059dc Mon Sep 17 00:00:00 2001 From: Jeavon Date: Tue, 4 Apr 2017 12:08:10 +0100 Subject: [PATCH 017/714] Tweaks to RelatedLinks - moved Content property from RelatedLinkBase to RelatedLink and populated Content property for non UDI RelatedLink editor --- src/Umbraco.Web/Models/RelatedLink.cs | 15 +++++++++------ src/Umbraco.Web/Models/RelatedLinkBase.cs | 3 --- .../RelatedLinksEditorValueConvertor.cs | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web/Models/RelatedLink.cs b/src/Umbraco.Web/Models/RelatedLink.cs index 884bdebeef..2dcb63dd5c 100644 --- a/src/Umbraco.Web/Models/RelatedLink.cs +++ b/src/Umbraco.Web/Models/RelatedLink.cs @@ -1,8 +1,11 @@ -namespace Umbraco.Web.Models +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models { - public class RelatedLink : RelatedLinkBase - { - public int? Id { get; internal set; } - internal bool IsDeleted { get; set; } - } + public class RelatedLink : RelatedLinkBase + { + public int? Id { get; internal set; } + internal bool IsDeleted { get; set; } + public IPublishedContent Content { get; set; } + } } diff --git a/src/Umbraco.Web/Models/RelatedLinkBase.cs b/src/Umbraco.Web/Models/RelatedLinkBase.cs index b347e25e0a..c2077ce4a9 100644 --- a/src/Umbraco.Web/Models/RelatedLinkBase.cs +++ b/src/Umbraco.Web/Models/RelatedLinkBase.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Umbraco.Core.Models; namespace Umbraco.Web.Models { @@ -15,7 +14,5 @@ namespace Umbraco.Web.Models public bool IsInternal { get; set; } [JsonProperty("type")] public RelatedLinkType Type { get; set; } - [JsonIgnore] - public IPublishedContent Content { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs index b95644d9ca..ae63c0a2b3 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs @@ -104,6 +104,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { relatedLink.Id = contentId; relatedLink = CreateLink(relatedLink); + relatedLink.Content = UmbracoContext.Current.ContentCache.GetById(contentId); } else { From 11b97c5291d01d292ffb02a4479b8a197f1cd379 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 20 Apr 2017 09:45:48 +0100 Subject: [PATCH 018/714] Removed unnecessary null check --- src/Umbraco.Core/Services/SectionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index 9de793e37d..dcfe3748ec 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -212,7 +212,7 @@ namespace Umbraco.Core.Services public void MakeNew(string name, string alias, string icon) { var sections = GetSections(); - var nextSortOrder = sections != null && sections.Any() ? sections.Max(x => x.SortOrder) + 1 : 1; + var nextSortOrder = sections.Any() ? sections.Max(x => x.SortOrder) + 1 : 1; MakeNew(name, alias, icon, nextSortOrder); } From 53ef2e356a0af8a7cb15029220d47c375bf65a28 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 3 May 2017 09:30:29 +0200 Subject: [PATCH 019/714] add new temp users tree --- src/Umbraco.Core/Constants-Applications.cs | 2 + src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 1 + src/Umbraco.Web/Trees/UsersTreeController.cs | 44 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 4 files changed, 48 insertions(+) create mode 100644 src/Umbraco.Web/Trees/UsersTreeController.cs diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index fd9476a8ab..8b9be02f4f 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -118,6 +118,8 @@ public const string Scripts = "scripts"; + public const string Users = "usersV2"; + //TODO: Fill in the rest! } } diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index ecea653650..777728582f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1370,6 +1370,7 @@ To manage your website, simply open the Umbraco back office and start adding con Templates XSLT Files Analytics + Users New update ready diff --git a/src/Umbraco.Web/Trees/UsersTreeController.cs b/src/Umbraco.Web/Trees/UsersTreeController.cs new file mode 100644 index 0000000000..65c289006a --- /dev/null +++ b/src/Umbraco.Web/Trees/UsersTreeController.cs @@ -0,0 +1,44 @@ +using System.Net.Http.Formatting; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.Users)] + [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 3)] + [PluginController("UmbracoTrees")] + [CoreTree] + public class UsersTreeController : TreeController + { + /// + /// Helper method to create a root model for a tree + /// + /// + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) + { + var root = base.CreateRootNode(queryStrings); + + //this will load in a custom UI instead of the dashboard for the root node + root.RoutePath = string.Format("{0}/{1}/{2}", Constants.Applications.Users, Constants.Trees.Users, "overview"); + root.Icon = "icon-users"; + + return root; + } + + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var baseUrl = Constants.Applications.Users + "/users/"; + + var nodes = new TreeNodeCollection(); + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + return menu; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d3e31fa366..d534dc2ae4 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -433,6 +433,7 @@ + From 8f69efe331295b9c4e8b3d9304dae434c076324b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 4 May 2017 19:16:52 +1000 Subject: [PATCH 020/714] It builds! --- .../Initial/DatabaseSchemaCreation.cs | 2 +- .../AddUserGroupTables.cs | 6 +- .../Repositories/PermissionRepository.cs | 158 ++++++++---------- .../Repositories/UserGroupRepository.cs | 2 +- .../Repositories/UserRepository.cs | 6 +- src/Umbraco.Core/Services/UserService.cs | 23 +-- src/Umbraco.Core/Umbraco.Core.csproj | 2 +- .../Cache/CacheRefresherEventHandlerTests.cs | 4 +- .../Repositories/ContentRepositoryTest.cs | 2 +- .../Repositories/UserGroupRepositoryTest.cs | 2 +- .../Repositories/UserRepositoryTest.cs | 7 +- .../Scoping/ScopedRepositoryTests.cs | 3 +- .../Services/UserServiceTests.cs | 11 +- .../Cache/CacheRefresherEventHandler.cs | 28 +--- src/Umbraco.Web/Umbraco.Web.csproj | 2 + .../umbraco/users/EditUser.aspx.cs | 14 +- 16 files changed, 121 insertions(+), 151 deletions(-) rename src/Umbraco.Core/Persistence/Migrations/Upgrades/{TargetVersionSevenSixZero => TargetVersionSevenSevenZero}/AddUserGroupTables.cs (99%) diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 609087c4bc..c4db9eacf9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -85,7 +85,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial //46, removed: UmbracoDeployChecksumDto //47, removed: UmbracoDeployDependencyDto {48, typeof (RedirectUrlDto) }, - {49, typeof (LockDto) } + {49, typeof (LockDto) }, {49, typeof (UserGroupDto) }, {50, typeof (User2UserGroupDto) }, {51, typeof (UserGroup2NodePermissionDto) }, diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs similarity index 99% rename from src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs rename to src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index ea566c0238..62b75bbf7a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -1,13 +1,13 @@ using System; +using System.Data; using System.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.SqlSyntax; -using System.Data; -namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero { - [Migration("7.6.0", 100, GlobalSettings.UmbracoMigrationName)] + [Migration("7.6.0", 100, Constants.System.UmbracoMigrationName)] public class AddUserGroupTables : MigrationBase { public AddUserGroupTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index 5cbffdeee5..3c7273eb0b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -1,7 +1,5 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Dynamic; using System.Globalization; using System.Linq; using System.Text; @@ -12,18 +10,8 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; using CacheKeys = Umbraco.Core.Cache.CacheKeys; using Umbraco.Core.Cache; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence.SqlSyntax; -using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { @@ -67,8 +55,8 @@ namespace Umbraco.Core.Persistence.Repositories sql.Select("*") .From() .Where(whereCriteria); - var result = _unitOfWork.Database.Fetch(sql).ToArray(); - // ToArray() to ensure it's all fetched from the db once + var result = _unitOfWork.Database.Fetch(sql).ToArray(); + // ToArray() to ensure it's all fetched from the db once return ConvertToPermissionList(result); }, GetCacheTimeout(), @@ -154,8 +142,8 @@ namespace Umbraco.Core.Persistence.Repositories .Where(dto => dto.NodeId == entityId) .OrderBy(dto => dto.NodeId); - var result = _unitOfWork.Database.Fetch(sql).ToArray(); - // ToArray() to ensure it's all fetched from the db once + var result = _unitOfWork.Database.Fetch(sql).ToArray(); + // ToArray() to ensure it's all fetched from the db once return ConvertToPermissionList(result); } @@ -170,37 +158,34 @@ namespace Umbraco.Core.Persistence.Repositories /// public void ReplacePermissions(int groupId, IEnumerable permissions, params int[] entityIds) { - var db = _unitOfWork.Database; - - //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)"; + var db = _unitOfWork.Database; + + //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)"; foreach (var idGroup in entityIds.InGroupsOf(2000)) - { - db.Execute(sql, new {groupId = groupId, nodeIds = idGroup}); - } - - var toInsert = new List(); + { + db.Execute(sql, new { groupId = groupId, nodeIds = idGroup }); + } + + var toInsert = new List(); foreach (var p in permissions) { foreach (var e in entityIds) - { - toInsert.Add(new UserGroup2NodePermissionDto + { + toInsert.Add(new UserGroup2NodePermissionDto { NodeId = e, - Permission = p.ToString(CultureInfo.InvariantCulture), - UserGroupId = groupId + Permission = p.ToString(CultureInfo.InvariantCulture), + UserGroupId = groupId }); } } - _unitOfWork.Database.BulkInsertRecords(toInsert, _sqlSyntax); - - //Raise the event - //TODO: FIX THIS - _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(toInsert), false)); - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(toInsert), false), this) - + _unitOfWork.Database.BulkInsertRecords(toInsert, _sqlSyntax); + + //Raise the event + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(toInsert), false)); + } ///
@@ -211,30 +196,27 @@ namespace Umbraco.Core.Persistence.Repositories /// public void AssignPermission(int groupId, char permission, params int[] entityIds) { - var db = _unitOfWork.Database; - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)"; - db.Execute(sql, - new - { - groupId = groupId, - permission = permission.ToString(CultureInfo.InvariantCulture), - entityIds = entityIds - }); - - var actions = entityIds.Select(id => new UserGroup2NodePermissionDto + var db = _unitOfWork.Database; + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)"; + db.Execute(sql, + new + { + groupId = groupId, + permission = permission.ToString(CultureInfo.InvariantCulture), + entityIds = entityIds + }); + + var actions = entityIds.Select(id => new UserGroup2NodePermissionDto { NodeId = id, - Permission = permission.ToString(CultureInfo.InvariantCulture), - UserGroupId = groupId + Permission = permission.ToString(CultureInfo.InvariantCulture), + UserGroupId = groupId }).ToArray(); - _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); - - //Raise the event - //TODO: Fix this - _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this) + _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); + + //Raise the event + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } @@ -246,31 +228,28 @@ namespace Umbraco.Core.Persistence.Repositories /// public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) { - var db = _unitOfWork.Database; - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)"; - db.Execute(sql, - new - { - nodeId = entity.Id, - permission = permission.ToString(CultureInfo.InvariantCulture), - groupIds = groupIds - }); - - var actions = groupIds.Select(id => new UserGroup2NodePermissionDto + var db = _unitOfWork.Database; + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)"; + db.Execute(sql, + new + { + nodeId = entity.Id, + permission = permission.ToString(CultureInfo.InvariantCulture), + groupIds = groupIds + }); + + var actions = groupIds.Select(id => new UserGroup2NodePermissionDto { NodeId = entity.Id, - Permission = permission.ToString(CultureInfo.InvariantCulture), - UserGroupId = id + Permission = permission.ToString(CultureInfo.InvariantCulture), + UserGroupId = id }).ToArray(); _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); //Raise the event - //TODO: Fix this - _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this) - + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); + } /// @@ -283,26 +262,23 @@ namespace Umbraco.Core.Persistence.Repositories /// public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) { - var db = _unitOfWork.Database; - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId"; - db.Execute(sql, new {nodeId = permissionSet.EntityId}); - - var actions = permissionSet.PermissionsSet.Select(p => new UserGroup2NodePermissionDto + var db = _unitOfWork.Database; + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId"; + db.Execute(sql, new { nodeId = permissionSet.EntityId }); + + var actions = permissionSet.PermissionsSet.Select(p => new UserGroup2NodePermissionDto { NodeId = permissionSet.EntityId, - Permission = p.Permission, - UserGroupId = p.UserGroupId + Permission = p.Permission, + UserGroupId = p.UserGroupId }).ToArray(); - _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); - - //Raise the event - //TODO: Fix this - _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this) - - } + _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); + + //Raise the event + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); + + } public static event TypedEventHandler, SaveEventArgs> AssignedPermissions; diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index d214c0d555..f87561e32f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly CacheHelper _cacheHelper; - public UserGroupRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public UserGroupRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cacheHelper, logger, sqlSyntax) { _cacheHelper = cacheHelper; diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 994c07d13e..7456bfc88c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -20,12 +20,12 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class UserRepository : PetaPocoRepositoryBase, IUserRepository { - private readonly CacheHelper _cacheHelper; + //private readonly CacheHelper _cacheHelper; - public UserRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public UserRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cacheHelper, logger, sqlSyntax) { - _cacheHelper = cacheHelper; + //_cacheHelper = cacheHelper; } #region Overrides of RepositoryBase diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 066823ca76..0c1e1fa63f 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -612,7 +612,7 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.GetUnitOfWork()) { - var repository = RepositoryFactory.CreateUserGroupRepository(uow) + var repository = RepositoryFactory.CreateUserGroupRepository(uow); repository.AssignGroupPermission(groupId, permission, entityIds); uow.Commit(); } @@ -641,7 +641,7 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var repository = RepositoryFactory.CreateUserGroupRepository(uow); + var repository = RepositoryFactory.CreateUserRepository(uow); return repository.GetGroupsForUser(userId); } } @@ -653,9 +653,12 @@ namespace Umbraco.Core.Services /// public IUserGroup GetUserGroupByAlias(string alias) { - using (var repository = RepositoryFactory.CreateUserGroupRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateUserGroupRepository(uow); var query = Query.Builder.Where(x => x.Alias == alias); + var contents = repository.GetByQuery(query); + return contents.SingleOrDefault(); } } @@ -682,7 +685,7 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var repository = RepositoryFactory.CreateUserGroupRepository(uow) + var repository = RepositoryFactory.CreateUserGroupRepository(uow); var query = Query.Builder.Where(x => x.Name == name); return repository.GetByQuery(query).SingleOrDefault(); } @@ -835,7 +838,7 @@ namespace Umbraco.Core.Services public IEnumerable GetPermissions(IUserGroup group, bool directlyAssignedOnly, params int[] nodeIds) { - using (var uow = UowProvider.GetUnitOfWork();) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); var explicitPermissions = repository.GetPermissionsForEntities(group.Id, nodeIds); @@ -951,11 +954,11 @@ namespace Umbraco.Core.Services : int.Parse(path); } - private static bool IsNotNullActionPermission(EntityPermission x) - { - const string NullActionChar = "-"; - return string.Join(string.Empty, x.AssignedPermissions) != NullActionChar; - } + //private static bool IsNotNullActionPermission(EntityPermission x) + //{ + // const string NullActionChar = "-"; + // return string.Join(string.Empty, x.AssignedPermissions) != NullActionChar; + //} /// /// Checks in a set of permissions associated with a user for those related to a given nodeId diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c6ccd8495e..1f3f352292 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -521,13 +521,13 @@ - + diff --git a/src/Umbraco.Tests/Cache/CacheRefresherEventHandlerTests.cs b/src/Umbraco.Tests/Cache/CacheRefresherEventHandlerTests.cs index a5036188fd..61f526a046 100644 --- a/src/Umbraco.Tests/Cache/CacheRefresherEventHandlerTests.cs +++ b/src/Umbraco.Tests/Cache/CacheRefresherEventHandlerTests.cs @@ -32,8 +32,8 @@ namespace Umbraco.Tests.Cache new EventDefinition(null, ServiceContext.SectionService, new EventArgs(), "Deleted"), new EventDefinition(null, ServiceContext.SectionService, new EventArgs(), "New"), - new EventDefinition>(null, ServiceContext.UserService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, ServiceContext.UserService, new DeleteEventArgs(Enumerable.Empty())), + new EventDefinition>(null, ServiceContext.UserService, new SaveEventArgs(Enumerable.Empty())), + new EventDefinition>(null, ServiceContext.UserService, new DeleteEventArgs(Enumerable.Empty())), new EventDefinition>(null, ServiceContext.UserService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, ServiceContext.UserService, new DeleteEventArgs(Enumerable.Empty())), diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index f8ff391c32..a3e5510941 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -64,7 +64,7 @@ namespace Umbraco.Tests.Persistence.Repositories return repository; } - private UserGroupRepository CreateUserGroupRepository(IDatabaseUnitOfWork unitOfWork) + private UserGroupRepository CreateUserGroupRepository(IScopeUnitOfWork unitOfWork) { return new UserGroupRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs index 6a2412bc63..1b669fdf60 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs @@ -30,7 +30,7 @@ namespace Umbraco.Tests.Persistence.Repositories base.TearDown(); } - private UserGroupRepository CreateRepository(IDatabaseUnitOfWork unitOfWork) + private UserGroupRepository CreateRepository(IScopeUnitOfWork unitOfWork) { return new UserGroupRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index f13175f563..62b22f681a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -28,14 +28,13 @@ namespace Umbraco.Tests.Persistence.Repositories base.TearDown(); } - private UserRepository CreateRepository(IScopeUnitOfWork unitOfWork, out UserTypeRepository userTypeRepository) + private UserRepository CreateRepository(IScopeUnitOfWork unitOfWork) { - userTypeRepository = new UserTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); - var repository = new UserRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, userTypeRepository); + var repository = new UserRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); return repository; } - private UserGroupRepository CreateUserGroupRepository(IDatabaseUnitOfWork unitOfWork) + private UserGroupRepository CreateUserGroupRepository(IScopeUnitOfWork unitOfWork) { return new UserGroupRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); } diff --git a/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs index 62bd17e122..ddbdf82f7f 100644 --- a/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs @@ -80,8 +80,7 @@ namespace Umbraco.Tests.Scoping var service = ApplicationContext.Services.UserService; var globalCache = ApplicationContext.ApplicationCache.IsolatedRuntimeCache.GetOrCreateCache(typeof(IUser)); - var userType = service.GetUserTypeByAlias("admin"); - var user = (IUser) new User("name", "email", "username", "rawPassword", userType); + var user = (IUser) new User("name", "email", "username", "rawPassword"); service.Save(user); // global cache contains the entity diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index e05b9e6225..6c513e0956 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -535,11 +535,10 @@ namespace Umbraco.Tests.Services public void Cannot_Create_User_With_Empty_Username() { // Arrange - var userService = ServiceContext.UserService; - var userType = userService.GetUserTypeByAlias("admin"); + var userService = ServiceContext.UserService; // Act & Assert - Assert.Throws(() => userService.CreateUserWithIdentity(string.Empty, "john@umbraco.io", userType)); + Assert.Throws(() => userService.CreateUserWithIdentity(string.Empty, "john@umbraco.io")); } [Test] @@ -547,8 +546,7 @@ namespace Umbraco.Tests.Services { // Arrange var userService = ServiceContext.UserService; - var userType = userService.GetUserTypeByAlias("admin"); - var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io", userType); + var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io"); user.Username = string.Empty; // Act & Assert @@ -560,8 +558,7 @@ namespace Umbraco.Tests.Services { // Arrange var userService = ServiceContext.UserService; - var userType = userService.GetUserTypeByAlias("admin"); - var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io", userType); + var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io"); user.Name = string.Empty; // Act & Assert diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 03898f8af4..2a30a90635 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -58,30 +58,12 @@ namespace Umbraco.Web.Cache () => SectionService.Deleted -= SectionService_Deleted); Bind(() => SectionService.New += SectionService_New, () => SectionService.New -= SectionService_New); - //bind to user / user group events - //TODO: FIX THIS - UserService.SavedUserGroup += UserServiceSavedUserGroup; - UserService.DeletedUserGroup += UserServiceDeletedUserGroup; - UserService.SavedUser += UserServiceSavedUser; - UserService.DeletedUser += UserServiceDeletedUser; - - //Bind to dictionary events - - LocalizationService.DeletedDictionaryItem += LocalizationServiceDeletedDictionaryItem; - LocalizationService.SavedDictionaryItem += LocalizationServiceSavedDictionaryItem; - - //Bind to data type events - //NOTE: we need to bind to legacy and new API events currently: http://issues.umbraco.org/issue/U4-1979 - - DataTypeService.Deleted += DataTypeServiceDeleted; - DataTypeService.Saved += DataTypeServiceSaved - //END FIX THIS - + // bind to user and user type events - Bind(() => UserService.SavedUserType += UserService_SavedUserType, - () => UserService.SavedUserType -= UserService_SavedUserType); - Bind(() => UserService.DeletedUserType += UserService_DeletedUserType, - () => UserService.DeletedUserType -= UserService_DeletedUserType); + Bind(() => UserService.SavedUserGroup += UserServiceSavedUserGroup, + () => UserService.SavedUserGroup -= UserServiceSavedUserGroup); + Bind(() => UserService.DeletedUserGroup += UserServiceDeletedUserGroup, + () => UserService.DeletedUserGroup -= UserServiceDeletedUserGroup); Bind(() => UserService.SavedUser += UserService_SavedUser, () => UserService.SavedUser -= UserService_SavedUser); Bind(() => UserService.DeletedUser += UserService_DeletedUser, diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8c22fec893..e8c92893fa 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -443,6 +443,8 @@ + ASPXCodeBehind + EditUserGroup.aspx diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index 7331cdb6f5..de60960e5a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -25,6 +25,7 @@ using Umbraco.Core.Models; using PropertyType = umbraco.cms.businesslogic.propertytype.PropertyType; using System.Text.RegularExpressions; using System.Text; +using Umbraco.Core.Security; namespace umbraco.cms.presentation.user { @@ -62,7 +63,7 @@ namespace umbraco.cms.presentation.user protected DropDownList cExcerpt = new DropDownList(); protected ContentPicker cMediaPicker = new ContentPicker(); protected ContentPicker cContentPicker = new ContentPicker(); - protected CustomValidator sectionValidator = new CustomValidator(); + //protected CustomValidator sectionValidator = new CustomValidator(); protected UpdatePanel pnlGroups = new UpdatePanel(); protected PlaceHolder pnlGroupControls = new PlaceHolder(); @@ -294,6 +295,17 @@ namespace umbraco.cms.presentation.user ClientTools .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) .SyncTree(UID.ToString(), IsPostBack); + } + + private void LnameCustomValidator_OnServerValidate(object source, ServerValidateEventArgs args) + { + var usersWithLoginName = ApplicationContext.Services.UserService.GetByUsername(lname.Text); + args.IsValid = usersWithLoginName == null || usersWithLoginName.Id == u.Id; + } + + private void EmailCustomValidator_OnServerValidate(object source, ServerValidateEventArgs args) + { + args.IsValid = MembershipProviderBase.IsEmailValid(email.Text.Trim()); } private void BindGroups() From 0bbef8cf949558ee2fcf6bfd74533a69a91fb2b7 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 May 2017 11:22:04 +0200 Subject: [PATCH 021/714] add temp resource for hardcoded data --- .../src/common/resources/users.resource.js | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js new file mode 100644 index 0000000000..9b7ffb6750 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -0,0 +1,101 @@ +/** + * @ngdoc service + * @name umbraco.resources.usersResource + * @function + * + * @description + * Used by the users section to get users and send requests to create, invite, delete, etc. users. + */ +(function () { + 'use strict'; + + function usersResource($http, umbRequestHelper, $q) { + + function getUsers() { + var deferred = $q.defer(); + var users = [ + { + "name": "Tammy Contreras", + "userGroupName": "Admin", + "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/adellecharles/128.jpg", + "state": "active" + }, + { + "name": "Edward Flores", + "userGroupName": "Admin", + "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/marcosmoralez/128.jpg", + "state": "active" + }, + { + "name": "Benjamin Mills", + "userGroupName": "Writer", + "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/dancounsell/128.jpg", + "state": "disabled" + }, + { + "name": "Samantha Martinez", + "userGroupName": "Editor", + "avatar": "", + "state": "pending" + }, + { + "name": "Angela Stone", + "userGroupName": "Editor", + "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/jina/128.jpg", + "state": "active" + }, + { + "name": "Beverly Silva", + "userGroupName": "Editor", + "avatar": "", + "state": "active" + }, + { + "name": "Arthur Welch", + "userGroupName": "Editor", + "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/ashleyford/128.jpg", + "state": "active" + }, + { + "name": "Ruth Turner", + "userGroupName": "Translator", + "avatar": "", + "state": "pending" + } + ]; + deferred.resolve(users); + return deferred.promise; + } + + function getUserGroups() { + var deferred = $q.defer(); + var userGroups = [ + { + "name": "Admin" + }, + { + "name": "Writer" + }, + { + "name": "Editor" + }, + { + "name": "Translator" + } + ]; + deferred.resolve(userGroups); + return deferred.promise; + } + + var resource = { + getUsers: getUsers, + getUserGroups: getUserGroups + }; + + return resource; + + } + + angular.module('umbraco.resources').factory('usersResource', usersResource); + +})(); From c8813c8584a5e8c43d38d5d17a1aaa149ca39b4f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 May 2017 11:23:38 +0200 Subject: [PATCH 022/714] add id to user groups data --- .../src/common/resources/users.resource.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 9b7ffb6750..a9ac7d5b8d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -71,16 +71,20 @@ var deferred = $q.defer(); var userGroups = [ { - "name": "Admin" + "name": "Admin", + "id": 1 }, { - "name": "Writer" + "name": "Writer", + "id": 2 }, { - "name": "Editor" + "name": "Editor", + "id": 3 }, { - "name": "Translator" + "name": "Translator", + "id": 4 } ]; deferred.resolve(userGroups); From e327a971c26a23cd8fac67407ac4e871067a3d61 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 May 2017 11:25:20 +0200 Subject: [PATCH 023/714] update umb-avatar component include name and colors --- .../components/umbavatar.directive.js | 25 ++++++- .../src/less/components/umb-avatar.less | 68 +++++++++++++++++-- .../src/views/components/umb-avatar.html | 8 ++- 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js index ba34a752ed..51c8d660db 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js @@ -52,15 +52,38 @@ Use this directive to render an avatar. function AvatarDirective() { + function link(scope, element, attrs, ctrl) { + + scope.initials = ""; + + function onInit() { + scope.initials = getNameInitials(scope.name); + } + + function getNameInitials(name) { + if(name) { + var initials = name.match(/\b\w/g) || []; + initials = ((initials.shift() || '') + (initials.pop() || '')).toUpperCase(); + return initials; + } + } + + onInit(); + + } + var directive = { restrict: 'E', replace: true, templateUrl: 'views/components/umb-avatar.html', scope: { size: "@", + name: "@", + color: "@", imgSrc: "@", imgSrcset: "@" - } + }, + link: link }; return directive; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less index c4414c2880..61441f16b0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less @@ -2,29 +2,87 @@ border-radius: 50%; width: 50px; height: 50px; + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + color: @black; + font-weight: bold; + font-size: 16px; + box-sizing: border-box; } -.umb-avatar.-xs { +/* Sizes */ + +.umb-avatar--xxs { + width: 26px; + height: 26px; + font-size: 12px; +} + +.umb-avatar--xs { width: 30px; height: 30px; + font-size: 12px; } -.umb-avatar.-s { +.umb-avatar--s { width: 40px; height: 40px; + font-size: 14px; } -.umb-avatar.-m { +.umb-avatar--m { width: 50px; height: 50px; + font-size: 16px; } -.umb-avatar.-l { +.umb-avatar--l { width: 70px; height: 70px; + font-size: 18px; } -.umb-avatar.-xl { +.umb-avatar--xl { width: 100px; height: 100px; + font-size: 20px; +} + +/* Colors */ + +.umb-avatar--white { + background-color: @white; + color: @black; +} + +.umb-avatar--gray { + background-color: @gray-10; + color: @black; +} + +.umb-avatar--primary { + background-color: @turquoise-l1; + color: @white; +} + +.umb-avatar--secondary { + background-color: @purple-l1; + color: @white; +} + +.umb-avatar--success { + background-color: @green-l1; + color: @white; +} + +.umb-avatar--warning { + background-color: @yellow-l1; + color: @white; +} + +.umb-avatar--danger { + background-color: @red-l1; + color: @white; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-avatar.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-avatar.html index 1d23769db2..c589eabcf7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-avatar.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-avatar.html @@ -1 +1,7 @@ - +
+ +
+ {{ initials }} + ? +
+
\ No newline at end of file From 7001e791f791505b9d79e0241e76df72955ebe3e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 May 2017 11:29:36 +0200 Subject: [PATCH 024/714] add umb-badge component --- .../components/umbbadge.directive.js | 23 +++++++ src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../src/less/components/umb-badge.less | 65 +++++++++++++++++++ .../src/views/components/umb-badge.html | 1 + 4 files changed, 90 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/umbbadge.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/umb-badge.html diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbbadge.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbbadge.directive.js new file mode 100644 index 0000000000..ee2c53eba0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbbadge.directive.js @@ -0,0 +1,23 @@ +(function() { + 'use strict'; + + function BadgeDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/umb-badge.html', + scope: { + size: "@?", + color: "@?" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbBadge', BadgeDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index cfe82ff3ea..7367e9cffd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -120,6 +120,7 @@ @import "components/umb-querybuilder.less"; @import "components/umb-pagination.less"; @import "components/umb-mini-list-view.less"; +@import "components/umb-badge.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less new file mode 100644 index 0000000000..152eedc83f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less @@ -0,0 +1,65 @@ +.umb-badge { + padding: 6px 8px; + margin: 0 5px 0 0; + font-weight: 600; + color: @black; + background-color: @turquoise-washed; + border-width: 1px; + border-style: solid; + border-color: @turquoise; + display: inline-flex; + align-items: center; + justify-content: center; +} + +// Colors +.umb-badge--primary { + background-color: @turquoise-washed; + border-color: @turquoise; +} + +.umb-badge--seconday { + background-color: @purple-washed; + border-color: @purple; +} + +.umb-badge--danger { + background-color: @red-washed; + border-color: @red; +} + +.umb-badge--warning { + background-color: @yellow-washed; + border-color: @yellow; +} + +.umb-badge--success { + background-color: @green-washed; + border-color: @green; +} + +// Size +.umb-badge--xs { + font-size: 13px; + padding: 1px 6px; +} + +.umb-badge--s { + font-size: 14px; + padding: 3px 6px; +} + +.umb-badge--m { + font-size: 16px; + padding: 6px 8px; +} + +.umb-badge--l { + font-size: 18px; + padding: 6px 8px; +} + +.umb-badge--xl { + font-size: 20px; + padding: 6px 8px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-badge.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-badge.html new file mode 100644 index 0000000000..1fd0c91520 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-badge.html @@ -0,0 +1 @@ + \ No newline at end of file From e7137e9cce56f6256d4eb1b6165cc8ad3816c9f3 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 May 2017 11:34:09 +0200 Subject: [PATCH 025/714] first steps on user dashboard --- src/Umbraco.Web.UI.Client/src/less/belle.less | 2 + .../src/less/components/users/umb-users.less | 38 +++++ src/Umbraco.Web.UI.Client/src/less/main.less | 11 ++ .../components/html/umb-control-group.html | 6 +- .../src/views/usersV2/overview.controller.js | 64 +++++++++ .../src/views/usersV2/overview.html | 134 ++++++++++++++++++ 6 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 7367e9cffd..de67e6d00f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -131,6 +131,8 @@ @import "components/umb-node-preview.less"; @import "components/umb-mini-editor.less"; +@import "components/users/umb-users.less"; + // Utilities @import "utilities/_flexbox.less"; @import "utilities/_spacing.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less new file mode 100644 index 0000000000..d65b3e7aae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less @@ -0,0 +1,38 @@ +.umb-users { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.umb-user { + flex: 1 1 200px; + background-color: @gray-10; + border: 1px solid @gray-9; + border-radius: 3px; + padding: 15px; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 10px; + cursor: pointer; +} + +.umb-user:hover { + border-color: @turquoise; +} + +.umb-user__avatar { + margin-bottom: 15px; +} + +.umb-user__name { + text-align: center; + font-size: 16px; + font-weight: bold; +} + +.umb-user__group { + +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 1b573bbe0b..32426ff263 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -113,6 +113,17 @@ h5.-black { border: none; } +/* BLOCK MODE */ +.block-form .umb-control-group { + border-bottom: none; + margin-bottom: 10px !important; + padding-bottom: 0; +} + +.block-form .umb-control-group label .help-block, +.block-form .umb-control-group label small { + font-size: 13px; +} /*COMPACT MODE */ .compact .umb-pane{margin: 0px 0px 15px 0px;} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html index b07c2b9c72..c16cf91834 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html @@ -3,11 +3,9 @@
- -
-
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js new file mode 100644 index 0000000000..9a4e4c1f5b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js @@ -0,0 +1,64 @@ +(function () { + "use strict"; + + function UsersOverviewController($scope, $timeout, usersResource) { + + var vm = this; + + vm.page = {}; + vm.page.name = "Users"; + vm.users = []; + vm.userGroups = []; + vm.usersViewState = 'overview'; + + vm.save = save; + vm.setUsersViewState = setUsersViewState; + vm.getUserStateType = getUserStateType; + + function init() { + + vm.loading = true; + + // Get users + usersResource.getUsers().then(function (users) { + vm.users = users; + }); + + // Get user groups + usersResource.getUserGroups().then(function (userGroups) { + vm.userGroups = userGroups; + }); + + // fake loading + $timeout(function () { + vm.loading = false; + }, 500); + + } + + function save() { + alert("save"); + } + + function setUsersViewState(state) { + vm.usersViewState = state; + } + + function getUserStateType(state) { + switch (state) { + case "disabled" || "umbracoDisabled": + return "danger"; + case "pending": + return "warning"; + default: + return "success"; + } + } + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Users.OverviewController", UsersOverviewController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html new file mode 100644 index 0000000000..a180cfdc82 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html @@ -0,0 +1,134 @@ +
+ + + +
+ + + + + + + + + + + +
+ + + + + + + +
+
+ + +
{{user.name}}
+
{{user.userGroupName}}
+ +
+
+
+ + +
+ + + + ← Take me back + + + +
+ +
+

Invite user

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non libero vel turpis ultrices pharetra.

+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + +
+ +
From 971f92301f3e3355682c5f472ce3e474c2be148d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 May 2017 11:45:58 +0200 Subject: [PATCH 026/714] add pagination to users --- .../src/views/usersV2/overview.controller.js | 4 ++++ .../src/views/usersV2/overview.html | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js index 9a4e4c1f5b..43bc252e8a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js @@ -10,6 +10,10 @@ vm.users = []; vm.userGroups = []; vm.usersViewState = 'overview'; + vm.usersPagination = { + "pageNumber": 1, + "totalPages": 5 + } vm.save = save; vm.setUsersViewState = setUsersViewState; diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html index a180cfdc82..9f26f377d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html @@ -62,6 +62,15 @@ --> + +
+ + +
+ From c0c1e2bc067dd2a2ab67ccab75dfddf4d354d325 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 8 May 2017 10:52:45 +0200 Subject: [PATCH 027/714] prototype user invite --- .../views/common/dialogs/login.controller.js | 19 +++++- .../src/views/common/dialogs/login.html | 58 ++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 49db7dc191..106e748c20 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -1,5 +1,20 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", - function ($scope, $cookies, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService) { + function ($scope, $cookies, $location, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService) { + + $scope.isInvite = false; + + function init() { + // Check if it is a new user + if ($location.search().invite) { + $scope.isInvite = true; + $scope.inviteSetPassword = true; + } + } + + $scope.inviteSavePassword = function() { + $scope.inviteSetPassword = false; + $scope.inviteSetAvatar = true; + }; var setFieldFocus = function(form, field) { $timeout(function() { @@ -234,4 +249,6 @@ $scope.showLogin(); } + init(); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 65ce1c3ecb..991b52cc62 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -1,5 +1,5 @@ 
- +
@@ -8,7 +8,61 @@
-
public UserController() : this(UmbracoContext.Current) - { + { } ///
@@ -24,7 +35,87 @@ namespace Umbraco.Web.Editors /// public UserController(UmbracoContext umbracoContext) : base(umbracoContext) - { + { + } + + /// + /// Gets a user by Id + /// + /// + /// + public UserDisplay GetById(int id) + { + var user = Services.UserService.GetUserById(id); + if (user == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + return Mapper.Map(user); + } + + //TODO: This will probably not be UserDisplay objects since there's probably too much data in the display object for a grid + public PagedResult GetPagedUsers( + int id, + int pageNumber = 1, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + string filter = "") + { + + //TODO: Make this real, for now this is mock data + + var startId = 100 + ((pageNumber -1) * pageSize); + var numUsers = pageSize; + var users = new List(); + var userTypes = Services.UserService.GetAllUserTypes().ToDictionary(x => x.Alias, x => x.Name); + var cultures = Services.TextService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); + for (int i = 0; i < numUsers; i++) + { + var display = new UserDisplay + { + Id = startId, + UserType = "writer", + AllowedSections = new[] {"content", "media"}, + AvailableUserTypes = userTypes, + Email = "test" + startId + "@test.com", + Name = "User " + startId, + Culture = "en-US", + AvailableCultures = cultures, + Path = "-1," + startId, + ParentId = -1, + StartContentId = -1, + StartMediaId = -1 + }; + users.Add(display); + startId++; + } + + return new PagedResult(100, pageNumber, pageSize) + { + Items = users + }; + } + + public UserDisplay PostSaveUser(UserSave userSave) + { + if (userSave == null) throw new ArgumentNullException("userSave"); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var intId = userSave.Id.TryConvertTo(); + if (intId.Success == false) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var found = Services.UserService.GetUserById(intId.Result); + if (found == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + //TODO: More validation, password changing logic, persisting + return Mapper.Map(found); } /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs b/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs index c1cf3ed5c6..07e800d647 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs @@ -1,14 +1,15 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Core.Models.Membership; namespace Umbraco.Web.Models.ContentEditing { /// - /// A basic structure the represents a user + /// A bare minimum structure that represents a user, usually attached to other objects /// [DataContract(Name = "user", Namespace = "")] - public class UserBasic : System.IComparable + public class UserBasic : IComparable { [DataMember(Name = "id", IsRequired = true)] [Required] @@ -19,9 +20,9 @@ namespace Umbraco.Web.Models.ContentEditing public string Name { get; set; } - int System.IComparable.CompareTo(object obj) + int IComparable.CompareTo(object obj) { - return Name.CompareTo(((UserBasic)obj).Name); + return String.Compare(Name, ((UserBasic)obj).Name, StringComparison.Ordinal); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index d27736576f..0b4bd5132d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -3,7 +3,10 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing -{ +{ + /// + /// Represents information for the current user + /// [DataContract(Name = "user", Namespace = "")] public class UserDetail : UserBasic { diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs new file mode 100644 index 0000000000..d77d9d2019 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Represents a user that is being edited + /// + [DataContract(Name = "user", Namespace = "")] + public class UserDisplay : EntityBasic, INotificationModel + { + public UserDisplay() + { + Notifications = new List(); + } + + [DataMember(Name = "culture", IsRequired = true)] + public string Culture { get; set; } + + [DataMember(Name = "email", IsRequired = true)] + public string Email { get; set; } + + [DataMember(Name = "userType")] + public string UserType { get; set; } + + /// + /// Gets the available user types (i.e. to populate a drop down) + /// The key is the Alias the value is the Name - the Alias is what is used in the UserType property and for persistence + /// + [DataMember(Name = "availableUserTypes")] + public IDictionary AvailableUserTypes { get; set; } + + /// + /// Gets the available cultures (i.e. to populate a drop down) + /// The key is the culture stored in the database, the value is the Name + /// + [DataMember(Name = "availableCultures")] + public IDictionary AvailableCultures { get; set; } + + [DataMember(Name = "startContentId")] + public int StartContentId { get; set; } + + [DataMember(Name = "startMediaId")] + public int StartMediaId { get; set; } + + /// + /// A list of sections the user is allowed to view. + /// + [DataMember(Name = "allowedSections")] + public IEnumerable AllowedSections { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs new file mode 100644 index 0000000000..5fe57e0787 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Represents the data used to persist a user + /// + /// + /// This will be different from the model used to display a user and we don't want to "Overpost" data back to the server, + /// and there will most likely be different bits of data required for updating passwords which will be different from the + /// data used to display vs save + /// + [DataContract(Name = "user", Namespace = "")] + public class UserSave : EntityBasic, IValidatableObject + { + //TODO: There will be more information to save along with the structure for changing passwords + + [DataMember(Name = "locale", IsRequired = true)] + [Required] + public string Culture { get; set; } + + [DataMember(Name = "email", IsRequired = true)] + [Required] + [EmailAddress] + public string Email { get; set; } + + [DataMember(Name = "userType")] + [Required] + public string UserType { get; set; } + + [DataMember(Name = "startContentId")] + public int StartContentId { get; set; } + + [DataMember(Name = "startMediaId")] + public int StartMediaId { get; set; } + + [DataMember(Name = "allowedSections")] + public IEnumerable AllowedSections { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + //TODO: Add other server side validation + //if (CultureInfo.GetCultureInfo(Culture)) + // yield return new ValidationResult("The culture is invalid", new[] { "Culture" }); + + yield break; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 72093b19a8..025de43d40 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models.Mapping; @@ -15,6 +16,16 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + config.CreateMap() + .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) + .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) + .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) + .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) + .ForMember( + detail => detail.AvailableUserTypes, + opt => opt.MapFrom(user => applicationContext.Services.SectionService.GetSections().ToDictionary(x => x.Alias, x => x.Name))); + config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => GetIntId(user.Id))) .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) diff --git a/src/Umbraco.Web/Trees/UsersTreeController.cs b/src/Umbraco.Web/Trees/UserTreeController.cs similarity index 77% rename from src/Umbraco.Web/Trees/UsersTreeController.cs rename to src/Umbraco.Web/Trees/UserTreeController.cs index 65c289006a..1318934559 100644 --- a/src/Umbraco.Web/Trees/UsersTreeController.cs +++ b/src/Umbraco.Web/Trees/UserTreeController.cs @@ -1,44 +1,57 @@ -using System.Net.Http.Formatting; -using Umbraco.Web.Models.Trees; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi.Filters; -using Constants = Umbraco.Core.Constants; - -namespace Umbraco.Web.Trees -{ - [UmbracoTreeAuthorize(Constants.Trees.Users)] - [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 3)] - [PluginController("UmbracoTrees")] - [CoreTree] - public class UsersTreeController : TreeController - { - /// - /// Helper method to create a root model for a tree - /// - /// - protected override TreeNode CreateRootNode(FormDataCollection queryStrings) - { - var root = base.CreateRootNode(queryStrings); - - //this will load in a custom UI instead of the dashboard for the root node - root.RoutePath = string.Format("{0}/{1}/{2}", Constants.Applications.Users, Constants.Trees.Users, "overview"); - root.Icon = "icon-users"; - - return root; - } - - protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) - { - var baseUrl = Constants.Applications.Users + "/users/"; - - var nodes = new TreeNodeCollection(); - return nodes; - } - - protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) - { - var menu = new MenuItemCollection(); - return menu; - } - } +using System.Net.Http.Formatting; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.Users)] + [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 3)] + [PluginController("UmbracoTrees")] + [CoreTree] + public class UserTreeController : TreeController + { + public UserTreeController() + { + } + + public UserTreeController(UmbracoContext umbracoContext) : base(umbracoContext) + { + } + + public UserTreeController(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + } + + /// + /// Helper method to create a root model for a tree + /// + /// + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) + { + var root = base.CreateRootNode(queryStrings); + + //this will load in a custom UI instead of the dashboard for the root node + root.RoutePath = string.Format("{0}/{1}/{2}", Constants.Applications.Users, Constants.Trees.Users, "overview"); + root.Icon = "icon-users"; + + root.HasChildren = false; + return root; + } + + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var baseUrl = Constants.Applications.Users + "/users/"; + + var nodes = new TreeNodeCollection(); + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + return menu; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d534dc2ae4..0c651af1fe 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -366,6 +366,8 @@ + + @@ -433,7 +435,7 @@ - + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs index 5db7a06dab..c749b6b58b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs @@ -34,7 +34,8 @@ namespace umbraco /// /// Handles loading of all umbraco users into the users application tree /// - [Tree(Constants.Applications.Users, "users", "Users")] + [Tree(Constants.Applications.Users, "users_old", "Users (Legacy)")] + //TODO: Remove this tree when ready public class loadUsers : BaseTree { public loadUsers(string application) : base(application) { } From 84c7f5046e76d90dd8dc52a85a03feb6d9418155 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 11 May 2017 15:15:46 +1000 Subject: [PATCH 046/714] Fixes the user group queries to be sorted since the relator requires this --- src/Umbraco.Core/Models/Rdbms/UserDto.cs | 5 ++ src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs | 5 ++ .../Persistence/Factories/UserGroupFactory.cs | 9 +-- .../Relators/UserGroupSectionRelator.cs | 10 +-- .../Repositories/UserGroupRepository.cs | 26 ++++---- .../Services/UserServiceExtensions.cs | 62 +++++++++---------- 6 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index ca308bbfee..11624e25f5 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -10,6 +10,11 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class UserDto { + public UserDto() + { + UserGroupDtos = new List(); + } + [Column("id")] [PrimaryKeyColumn(Name = "PK_user")] public int Id { get; set; } diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs index 11444a5a23..cadf7cbcdb 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -9,6 +9,11 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class UserGroupDto { + public UserGroupDto() + { + UserGroup2AppDtos = new List(); + } + [Column("id")] [PrimaryKeyColumn(IdentitySeed = 5)] public int Id { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs index 323ef3319a..0ca2f6cb8c 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs @@ -6,11 +6,9 @@ using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Factories { - internal class UserGroupFactory + internal static class UserGroupFactory { - #region Implementation of IEntityFactory - - public IUserGroup BuildEntity(UserGroupDto dto) + public static IUserGroup BuildEntity(UserGroupDto dto) { var userGroup = new UserGroup(); @@ -42,7 +40,7 @@ namespace Umbraco.Core.Persistence.Factories } } - public UserGroupDto BuildDto(IUserGroup entity) + public static UserGroupDto BuildDto(IUserGroup entity) { var dto = new UserGroupDto { @@ -72,6 +70,5 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Relators/UserGroupSectionRelator.cs b/src/Umbraco.Core/Persistence/Relators/UserGroupSectionRelator.cs index 03c5bcd398..9b96bf6a8d 100644 --- a/src/Umbraco.Core/Persistence/Relators/UserGroupSectionRelator.cs +++ b/src/Umbraco.Core/Persistence/Relators/UserGroupSectionRelator.cs @@ -15,12 +15,12 @@ namespace Umbraco.Core.Persistence.Relators if (a == null) return Current; - // Is this the same DictionaryItem as the current one we're processing + // Is this the same object as the current one we're processing if (Current != null && Current.Id == a.Id) { if (p.AppAlias.IsNullOrWhiteSpace() == false) { - // Yes, just add this UserGroup2AppDto to the current item's collection + // Yes, just add this to the current item's collection Current.UserGroup2AppDtos.Add(p); } @@ -28,13 +28,13 @@ namespace Umbraco.Core.Persistence.Relators return null; } - // This is a different user group to the current one, or this is the + // This is a different object to the current one, or this is the // first time through and we don't have one yet - // Save the current user group + // Save the current instance var prev = Current; - // Setup the new current user group + // Setup the new current instance Current = a; Current.UserGroup2AppDtos = new List(); //this can be null since we are doing a left join diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index f81725cc9b..d4476bd3c8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -74,6 +74,8 @@ namespace Umbraco.Core.Persistence.Repositories var innerSql = GetBaseQuery("umbracoUserGroup.id"); innerSql.Where("umbracoUserGroup2App.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); sql.Where(string.Format("umbracoUserGroup.id IN ({0})", innerSql.SQL)); + //must be included for relator to work + sql.OrderBy(x => x.Id, SqlSyntax); return ConvertFromDtos(Database.Fetch(new UserGroupSectionRelator().Map, sql)); } @@ -146,14 +148,15 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); + //must be included for relator to work + sql.OrderBy(x => x.Id, SqlSyntax); var dto = Database.Fetch(new UserGroupSectionRelator().Map, sql).FirstOrDefault(); if (dto == null) return null; - var userGroupFactory = new UserGroupFactory(); - var userGroup = userGroupFactory.BuildEntity(dto); + var userGroup = UserGroupFactory.BuildEntity(dto); return userGroup; } @@ -169,6 +172,8 @@ namespace Umbraco.Core.Persistence.Repositories { sql.Where(x => x.Id >= 0); } + //must be included for relator to work + sql.OrderBy(x => x.Id, SqlSyntax); var dtos = Database.Fetch(new UserGroupSectionRelator().Map, sql); return ConvertFromDtos(dtos); @@ -176,10 +181,11 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformGetByQuery(IQuery query) { - var userGroupFactory = new UserGroupFactory(); var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); + //must be included for relator to work + sql.OrderBy(x => x.Id, SqlSyntax); var dtos = Database.Fetch(new UserGroupSectionRelator().Map, sql); return ConvertFromDtos(dtos); @@ -238,8 +244,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistNewItem(IUserGroup entity) { - var userGroupFactory = new UserGroupFactory(); - var userGroupDto = userGroupFactory.BuildDto(entity); + var userGroupDto = UserGroupFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(userGroupDto)); entity.Id = id; @@ -249,8 +254,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistUpdatedItem(IUserGroup entity) { - var userGroupFactory = new UserGroupFactory(); - var userGroupDto = userGroupFactory.BuildDto(entity); + var userGroupDto = UserGroupFactory.BuildDto(entity); Database.Update(userGroupDto); @@ -279,13 +283,9 @@ namespace Umbraco.Core.Persistence.Repositories #endregion - private IEnumerable ConvertFromDtos(IEnumerable dtos) + private static IEnumerable ConvertFromDtos(IEnumerable dtos) { - return dtos.Select(dto => - { - var userGroupFactory = new UserGroupFactory(); - return userGroupFactory.BuildEntity(dto); - }); + return dtos.Select(UserGroupFactory.BuildEntity); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 0505500055..6130077771 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -27,40 +27,40 @@ namespace Umbraco.Core.Services userService.ReplaceUserGroupPermissions(groupId, new char[] { }); } - ///// - ///// Maps a custom provider's information to an umbraco user account - ///// - ///// - ///// - ///// - ///// To maintain compatibility we have to check the login name if the provider key lookup fails but otherwise - ///// we'll store the provider user key in the login column. - ///// - //public static IUser CreateUserMappingForCustomProvider(this IUserService userService, MembershipUser member) - //{ - // if (member == null) throw new ArgumentNullException("member"); + /// + /// Maps a custom provider's information to an umbraco user account + /// + /// + /// + /// + /// To maintain compatibility we have to check the login name if the provider key lookup fails but otherwise + /// we'll store the provider user key in the login column. + /// + public static IUser CreateUserMappingForCustomProvider(this IUserService userService, MembershipUser member) + { + if (member == null) throw new ArgumentNullException("member"); - // var valToLookup = member.ProviderUserKey == null ? member.UserName : member.ProviderUserKey.ToString(); - // var found = userService.GetByUsername(valToLookup); - // if (found == null && member.ProviderUserKey != null) - // { - // //try by username - // found = userService.GetByUsername(member.UserName); - // } + var valToLookup = member.ProviderUserKey == null ? member.UserName : member.ProviderUserKey.ToString(); + var found = userService.GetByUsername(valToLookup); + if (found == null && member.ProviderUserKey != null) + { + //try by username + found = userService.GetByUsername(member.UserName); + } - // if (found == null) - // { - // var user = new User( - // member.UserName, - // member.Email ?? Guid.NewGuid().ToString("N") + "@example.com", //email cannot be empty - // member.ProviderUserKey == null ? member.UserName : member.ProviderUserKey.ToString(), - // Guid.NewGuid().ToString("N")); //pass cannot be empty - // userService.Save(user); - // return user; - // } + if (found == null) + { + var user = new User( + member.UserName, + member.Email ?? Guid.NewGuid().ToString("N") + "@example.com", //email cannot be empty + member.ProviderUserKey == null ? member.UserName : member.ProviderUserKey.ToString(), + Guid.NewGuid().ToString("N")); //pass cannot be empty + userService.Save(user); + return user; + } - // return found; - //} + return found; + } } } \ No newline at end of file From e5240575bd8454ffee141febf86d0d2af12836a3 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 May 2017 09:31:58 +0200 Subject: [PATCH 047/714] add bulk selection to user roles --- .../usersV2/views/roles/roles.controller.js | 94 +++++++++++++++++++ .../src/views/usersV2/views/roles/roles.html | 72 +++++++++++++- 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.controller.js b/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.controller.js new file mode 100644 index 0000000000..79f16644d5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.controller.js @@ -0,0 +1,94 @@ +(function () { + "use strict"; + + function UserRolesController($scope, $timeout, $location, usersResource) { + + var vm = this; + + vm.userRoles = []; + vm.selection = []; + vm.viewState = 'overview'; + + vm.pagination = { + "pageNumber": 1, + "totalPages": 5 + }; + + vm.setViewState = setViewState; + vm.goToUserRole = goToUserRole; + vm.clearSelection = clearSelection; + vm.selectUserRole = selectUserRole; + vm.selectAll = selectAll; + vm.areAllSelected = areAllSelected; + + function init() { + + vm.loading = true; + + // Get users + usersResource.getUserGroups().then(function (userRoles) { + vm.userRoles = userRoles; + }); + + // fake loading + $timeout(function () { + vm.loading = false; + }, 500); + + } + + function setViewState(state) { + vm.viewState = state; + } + + function selectUserRole(userRole, selection) { + if(userRole.selected) { + var index = selection.indexOf(userRole.id); + selection.splice(index, 1); + userRole.selected = false; + } else { + userRole.selected = true; + vm.selection.push(userRole.id); + } + } + + function clearSelection() { + angular.forEach(vm.userRoles, function(userRole){ + userRole.selected = false; + }); + vm.selection = []; + } + + function goToUserRole(userRole) { + $location.path('users/usersV2/role/' + userRole.id); + } + function selectAll() { + if(areAllSelected()) { + vm.selection = []; + angular.forEach(vm.userRoles, function(userRole){ + userRole.selected = false; + }); + } else { + // clear selection so we don't add the same user twice + vm.selection = []; + // select all users + angular.forEach(vm.userRoles, function(userRole){ + userRole.selected = true; + vm.selection.push(userRole.id); + }); + } + } + + function areAllSelected() { + if(vm.selection.length === vm.userRoles.length) { + return true; + } + } + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Users.RolesController", UserRolesController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.html b/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.html index be73bace60..b90edb55dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.html +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.html @@ -1 +1,71 @@ -
This is my roles overview
\ No newline at end of file +
+ + + + + + + + + + + + + +
+ + +
+
+
+ +
+ + + + + + + + + {{ vm.selection.length }} of {{ vm.userRoles.length }} selected + + + + + + + + +
+ +
+ +
{{userRole.name}}
+ Edit +
+ +
\ No newline at end of file From 536be45bbf88bdb27e750ab2270e27f6e5593746 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 May 2017 09:47:35 +0200 Subject: [PATCH 048/714] add go back button to edit role --- .../src/views/usersV2/role.controller.js | 8 +++++++- src/Umbraco.Web.UI.Client/src/views/usersV2/role.html | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/role.controller.js b/src/Umbraco.Web.UI.Client/src/views/usersV2/role.controller.js index eb8cdb6a0c..53aa0c5319 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/role.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/role.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserRoleEditController($scope, $timeout, usersResource) { + function UserRoleEditController($scope, $timeout, $location, usersResource) { var vm = this; @@ -9,6 +9,8 @@ vm.page = {}; vm.userRole = {}; + vm.goBack = goBack; + function init() { vm.loading = true; @@ -24,6 +26,10 @@ }, 500); } + + function goBack() { + $location.path("users/usersV2/overview"); + } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/role.html b/src/Umbraco.Web.UI.Client/src/views/usersV2/role.html index ad4916074e..e51eda26b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/role.html +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/role.html @@ -15,7 +15,11 @@ - Edit role here + + + ← Back to roles + + From 956250004ebcba3bb946e484a8799972535e99c6 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 May 2017 09:48:16 +0200 Subject: [PATCH 049/714] add go back button to edit user --- .../src/views/usersV2/user.controller.js | 8 +++++++- src/Umbraco.Web.UI.Client/src/views/usersV2/user.html | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/usersV2/user.controller.js index 81e4e936fa..bb5a8b1a29 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, $timeout, usersResource) { + function UserEditController($scope, $timeout, $location, usersResource) { var vm = this; @@ -9,6 +9,8 @@ vm.page = {}; vm.user = {}; + vm.goBack = goBack; + function init() { vm.loading = true; @@ -24,6 +26,10 @@ }, 500); } + + function goBack() { + $location.path("users/usersV2/overview"); + } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/user.html b/src/Umbraco.Web.UI.Client/src/views/usersV2/user.html index dbc21a24e2..208a941200 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/user.html @@ -15,7 +15,11 @@ - Edit user here + + + ← Back to users + + From 03cce6e8f66a730b5be1504150784196567ac831 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 May 2017 09:53:19 +0200 Subject: [PATCH 050/714] move badge + add title --- .../src/less/components/users/umb-users.less | 8 +- .../src/views/usersV2/views/users/users.html | 102 +++++++++--------- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less index d557098b2c..7d4dde9e0c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less @@ -29,17 +29,15 @@ } .umb-user__avatar { - margin-bottom: 15px; + margin-bottom: 10px; margin-left: auto; margin-right: auto; - position: relative; } .umb-user__badge { position: absolute; - bottom: -10px; - left: 50%; - transform: translate(-50%, 0); + top: 10px; + left: 10px; text-transform: capitalize; } diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.html index fa34248c61..6d5f54a0c8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.html @@ -79,70 +79,76 @@ -
-
- - {{ user.state }} +
+ +
{{user.name}} From 4e76b61251d3b8dad0333135ef0873f749af7825 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 May 2017 09:53:43 +0200 Subject: [PATCH 051/714] rename section --- .../src/views/usersV2/overview.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js index 7a3bc8fc92..576d0db6ef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js @@ -6,7 +6,7 @@ var vm = this; vm.page = {}; - vm.page.name = "Users"; + vm.page.name = "User Management"; vm.page.navigation = [ { "name": "Users", From 6a4a84d8aacea047721ac92e84cfc8f832184ebd Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 May 2017 10:25:57 +0200 Subject: [PATCH 052/714] update files to match new tree path --- .../src/views/{usersV2 => users}/overview.controller.js | 0 .../src/views/{usersV2 => users}/overview.html | 0 .../src/views/{usersV2 => users}/role.controller.js | 2 +- .../src/views/{usersV2 => users}/role.html | 0 .../src/views/{usersV2 => users}/user.controller.js | 2 +- .../src/views/{usersV2 => users}/user.html | 0 .../views/{usersV2 => users}/views/roles/roles.controller.js | 2 +- .../src/views/{usersV2 => users}/views/roles/roles.html | 0 .../views/{usersV2 => users}/views/users/users.controller.js | 2 +- .../src/views/{usersV2 => users}/views/users/users.html | 0 10 files changed, 4 insertions(+), 4 deletions(-) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/overview.controller.js (100%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/overview.html (100%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/role.controller.js (93%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/role.html (100%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/user.controller.js (93%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/user.html (100%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/views/roles/roles.controller.js (97%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/views/roles/roles.html (100%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/views/users/users.controller.js (99%) rename src/Umbraco.Web.UI.Client/src/views/{usersV2 => users}/views/users/users.html (100%) diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/overview.controller.js rename to src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html b/src/Umbraco.Web.UI.Client/src/views/users/overview.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/overview.html rename to src/Umbraco.Web.UI.Client/src/views/users/overview.html diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/role.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js similarity index 93% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/role.controller.js rename to src/Umbraco.Web.UI.Client/src/views/users/role.controller.js index 53aa0c5319..3381fd81a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/role.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js @@ -28,7 +28,7 @@ } function goBack() { - $location.path("users/usersV2/overview"); + $location.path("users/users/overview"); } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/role.html b/src/Umbraco.Web.UI.Client/src/views/users/role.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/role.html rename to src/Umbraco.Web.UI.Client/src/views/users/role.html diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js similarity index 93% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/user.controller.js rename to src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index bb5a8b1a29..09e74c5455 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -28,7 +28,7 @@ } function goBack() { - $location.path("users/usersV2/overview"); + $location.path("users/users/overview"); } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/user.html rename to src/Umbraco.Web.UI.Client/src/views/users/user.html diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.controller.js similarity index 97% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.controller.js rename to src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.controller.js index 79f16644d5..6a602e4cfa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.controller.js @@ -60,7 +60,7 @@ } function goToUserRole(userRole) { - $location.path('users/usersV2/role/' + userRole.id); + $location.path('users/users/role/' + userRole.id); } function selectAll() { if(areAllSelected()) { diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.html b/src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/views/roles/roles.html rename to src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.html diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js similarity index 99% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.controller.js rename to src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 8ab31ea311..a18f91f22e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -132,7 +132,7 @@ } function goToUser(user, event) { - $location.path('users/usersV2/user/' + user.id); + $location.path('users/users/user/' + user.id); } function disableUser() { diff --git a/src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/usersV2/views/users/users.html rename to src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html From 9b50d6d2510fe2a1e6849438b9bc91f62ac4944c Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 11 May 2017 20:51:31 +1000 Subject: [PATCH 053/714] Gets a few tests fixed --- src/Umbraco.Core/Services/ContentService.cs | 2 +- .../Cache/CacheRefresherEventHandlerTests.cs | 8 +++----- src/Umbraco.Tests/Services/ContentServiceTests.cs | 6 ++++-- src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 282b5d097f..d2911a64d4 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -111,7 +111,7 @@ namespace Umbraco.Core.Services } /// - /// Assigns a single permission to the current content item for the specified user ids + /// Assigns a single permission to the current content item for the specified group ids /// /// /// diff --git a/src/Umbraco.Tests/Cache/CacheRefresherEventHandlerTests.cs b/src/Umbraco.Tests/Cache/CacheRefresherEventHandlerTests.cs index 61f526a046..c0ead52e79 100644 --- a/src/Umbraco.Tests/Cache/CacheRefresherEventHandlerTests.cs +++ b/src/Umbraco.Tests/Cache/CacheRefresherEventHandlerTests.cs @@ -31,13 +31,11 @@ namespace Umbraco.Tests.Cache new EventDefinition(null, ServiceContext.SectionService, new EventArgs(), "Deleted"), new EventDefinition(null, ServiceContext.SectionService, new EventArgs(), "New"), - - new EventDefinition>(null, ServiceContext.UserService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, ServiceContext.UserService, new DeleteEventArgs(Enumerable.Empty())), - + new EventDefinition>(null, ServiceContext.UserService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, ServiceContext.UserService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, ServiceContext.UserService, new DeleteEventArgs(Enumerable.Empty())), + new EventDefinition>(null, ServiceContext.UserService, new SaveEventArgs(Enumerable.Empty())), + new EventDefinition>(null, ServiceContext.UserService, new DeleteEventArgs(Enumerable.Empty())), new EventDefinition>(null, ServiceContext.LocalizationService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, ServiceContext.LocalizationService, new DeleteEventArgs(Enumerable.Empty())), diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index a6fb9d64ca..652152273d 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1403,9 +1403,11 @@ namespace Umbraco.Tests.Services })); Assert.IsTrue(ServiceContext.PublicAccessService.AddRule(content1, "test2", "test2").Success); - Assert.IsNotNull(ServiceContext.NotificationService.CreateNotification(ServiceContext.UserService.GetUserById(0), content1, "test")); + var user = ServiceContext.UserService.GetUserById(0); + var userGroup = ServiceContext.UserService.GetUserGroupByAlias(user.Groups.First()); + Assert.IsNotNull(ServiceContext.NotificationService.CreateNotification(user, content1, "test")); - ServiceContext.ContentService.AssignContentPermission(content1, 'A', new[] {0}); + ServiceContext.ContentService.AssignContentPermission(content1, 'A', new[] { userGroup.Id}); Assert.IsTrue(ServiceContext.DomainService.Save(new UmbracoDomain("www.test.com", "en-AU") { diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 2a30a90635..5705af511e 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -60,10 +60,10 @@ namespace Umbraco.Web.Cache () => SectionService.New -= SectionService_New); // bind to user and user type events - Bind(() => UserService.SavedUserGroup += UserServiceSavedUserGroup, - () => UserService.SavedUserGroup -= UserServiceSavedUserGroup); - Bind(() => UserService.DeletedUserGroup += UserServiceDeletedUserGroup, - () => UserService.DeletedUserGroup -= UserServiceDeletedUserGroup); + Bind(() => UserService.SavedUserGroup += UserService_SavedUserGroup, + () => UserService.SavedUserGroup -= UserService_SavedUserGroup); + Bind(() => UserService.DeletedUserGroup += UserService_DeletedUserGroup, + () => UserService.DeletedUserGroup -= UserService_DeletedUserGroup); Bind(() => UserService.SavedUser += UserService_SavedUser, () => UserService.SavedUser -= UserService_SavedUser); Bind(() => UserService.DeletedUser += UserService_DeletedUser, @@ -630,12 +630,12 @@ namespace Umbraco.Web.Cache e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserCache(x.Id)); } - static void UserServiceSavedUserGroup(IUserService sender, SaveEventArgs e) + static void UserService_SavedUserGroup(IUserService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshUserGroupCache(x.Id)); } - static void UserServiceDeletedUserGroup(IUserService sender, DeleteEventArgs e) + static void UserService_DeletedUserGroup(IUserService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserGroupCache(x.Id)); } From 7cf3f10afb3c1e4b2e5242c91d333cb7dad4a7ab Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 May 2017 13:44:35 +0200 Subject: [PATCH 054/714] missed two paths --- .../src/views/users/overview.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js index 576d0db6ef..ffee2a6775 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js @@ -11,13 +11,13 @@ { "name": "Users", "icon": "icon-user", - "view": "views/usersV2/views/users/users.html", + "view": "views/users/views/users/users.html", "active": true }, { "name": "Roles", "icon": "icon-users", - "view": "views/usersV2/views/roles/roles.html" + "view": "views/users/views/roles/roles.html" } ]; From 0e8b8e3aef86492fc55e87c1ca7521315413178e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 May 2017 13:45:18 +0200 Subject: [PATCH 055/714] set up some simple filtering of actions based on selection --- .../users/views/users/users.controller.js | 36 +++++++++++++++++++ .../src/views/users/views/users/users.html | 14 ++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index a18f91f22e..cd96c3893c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -13,6 +13,10 @@ vm.newUser.userGroups = []; vm.usersViewState = 'overview'; + vm.allowDisableUser = true; + vm.allowEnableUser = true; + vm.allowSetUserRole = true; + vm.usersPagination = { "pageNumber": 1, "totalPages": 5 @@ -122,6 +126,9 @@ user.selected = true; vm.selection.push(user.id); } + + setBulkActions(vm.users); + } function clearSelection() { @@ -223,6 +230,35 @@ }); } + function setBulkActions(users) { + + // reset all states + vm.allowDisableUser = true; + vm.allowEnableUser = true; + vm.allowSetUserGroup = true; + + angular.forEach(users, function(user){ + + if(!user.selected) { + return; + } + + if(user.state === "disabled") { + vm.allowDisableUser = false; + } + + if(user.state === "active") { + vm.allowEnableUser = false; + } + + if(user.state === "pending") { + vm.allowEnableUser = false; + } + + }); + } + + init(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 6d5f54a0c8..472e8747f8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -62,13 +62,23 @@ + action="vm.setUserRoleUser()"> + + Date: Mon, 15 May 2017 20:27:40 +1000 Subject: [PATCH 056/714] Updates UserGroupRepository to ensure that it persists things int he correct order and inside of a transaction, removes new code that is already obsolete, fixes tests --- .../Interfaces/IUserGroupRepository.cs | 14 +- .../Repositories/UserGroupRepository.cs | 142 ++++++-- src/Umbraco.Core/Services/IUserService.cs | 12 +- src/Umbraco.Core/Services/UserService.cs | 16 +- .../Repositories/UserRepositoryTest.cs | 11 +- .../Services/SectionServiceTests.cs | 5 +- .../Services/UserServiceTests.cs | 22 +- .../umbraco/config/create/UI.Release.xml | 8 - .../umbraco/config/create/UI.xml | 10 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 - .../umbraco/Trees/UserGroups.cs | 6 +- .../umbraco/users/EditUserGroup.aspx.cs | 27 +- .../umbraco/users/UserGroupTasks.cs | 42 --- src/umbraco.businesslogic/UserGroup.cs | 309 ------------------ .../umbraco.businesslogic.csproj | 1 - 15 files changed, 176 insertions(+), 450 deletions(-) delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupTasks.cs delete mode 100644 src/umbraco.businesslogic/UserGroup.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs index a26833a4ce..3aca34c5bd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs @@ -19,17 +19,11 @@ namespace Umbraco.Core.Persistence.Repositories IEnumerable GetGroupsAssignedToSection(string sectionAlias); /// - /// Removes all users from a group + /// Used to add or update a user group and assign users to it /// - /// Id of group - void RemoveAllUsersFromGroup(int groupId); - - /// - /// Adds a set of users to a group - /// - /// Id of group - /// Ids of users - void AddUsersToGroup(int groupId, int[] userIds); + /// + /// + void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[] userIds); ///
/// Gets the group permissions for the specified entities diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index d4476bd3c8..2cc8819da0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -4,6 +4,7 @@ using System.Linq; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Cache; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.Factories; @@ -20,11 +21,13 @@ namespace Umbraco.Core.Persistence.Repositories internal class UserGroupRepository : PetaPocoRepositoryBase, IUserGroupRepository { private readonly CacheHelper _cacheHelper; + private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; public UserGroupRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cacheHelper, logger, sqlSyntax) { _cacheHelper = cacheHelper; + _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, work, cacheHelper, logger, sqlSyntax); } public const string GetByAliasCacheKeyPrefix = "UserGroupRepository_GetByAlias_"; @@ -80,32 +83,11 @@ namespace Umbraco.Core.Persistence.Repositories return ConvertFromDtos(Database.Fetch(new UserGroupSectionRelator().Map, sql)); } - /// - /// Removes all users from a group - /// - /// Id of group - public void RemoveAllUsersFromGroup(int groupId) + public void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[] userIds) { - Database.Delete("WHERE userGroupId = @GroupId", new { GroupId = groupId }); + _userGroupWithUsersRepository.AddOrUpdate(new UserGroupWithUsers(userGroup, userIds)); } - /// - /// Adds a set of users to a group - /// - /// Id of group - /// Ids of users - public void AddUsersToGroup(int groupId, int[] userIds) - { - foreach (var userId in userIds) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupId, - UserId = userId, - }; - Database.Insert(dto); - } - } /// /// Gets the group permissions for the specified entities @@ -287,5 +269,119 @@ namespace Umbraco.Core.Persistence.Repositories { return dtos.Select(UserGroupFactory.BuildEntity); } + + /// + /// used to persist a user group with associated users at once + /// + private class UserGroupWithUsers : Entity, IAggregateRoot + { + public UserGroupWithUsers(IUserGroup userGroup, int[] userIds) + { + UserGroup = userGroup; + UserIds = userIds; + } + + public IUserGroup UserGroup { get; private set; } + public int[] UserIds { get; private set; } + + } + + /// + /// used to persist a user group with associated users at once + /// + private class UserGroupWithUsersRepository : PetaPocoRepositoryBase + { + private readonly UserGroupRepository _userGroupRepo; + + public UserGroupWithUsersRepository(UserGroupRepository userGroupRepo, IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + _userGroupRepo = userGroupRepo; + } + + #region Not implemented (don't need to for the purposes of this repo) + protected override UserGroupWithUsers PerformGet(int id) + { + throw new NotImplementedException(); + } + protected override IEnumerable PerformGetAll(params int[] ids) + { + throw new NotImplementedException(); + } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + protected override Sql GetBaseQuery(bool isCount) + { + throw new NotImplementedException(); + } + protected override string GetBaseWhereClause() + { + throw new NotImplementedException(); + } + protected override IEnumerable GetDeleteClauses() + { + throw new NotImplementedException(); + } + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + #endregion + + protected override void PersistNewItem(UserGroupWithUsers entity) + { + //save the user group + _userGroupRepo.PersistNewItem(entity.UserGroup); + if (entity.UserIds != null) + { + //now the user association + RemoveAllUsersFromGroup(entity.UserGroup.Id); + AddUsersToGroup(entity.UserGroup.Id, entity.UserIds); + } + + } + + protected override void PersistUpdatedItem(UserGroupWithUsers entity) + { + //save the user group + _userGroupRepo.PersistUpdatedItem(entity.UserGroup); + if (entity.UserIds != null) + { + //now the user association + RemoveAllUsersFromGroup(entity.UserGroup.Id); + AddUsersToGroup(entity.UserGroup.Id, entity.UserIds); + } + } + + /// + /// Removes all users from a group + /// + /// Id of group + private void RemoveAllUsersFromGroup(int groupId) + { + Database.Delete("WHERE userGroupId = @GroupId", new { GroupId = groupId }); + } + + /// + /// Adds a set of users to a group + /// + /// Id of group + /// Ids of users + private void AddUsersToGroup(int groupId, int[] userIds) + { + //TODO: Check if the user exists? + foreach (var userId in userIds) + { + var dto = new User2UserGroupDto + { + UserGroupId = groupId, + UserId = userId, + }; + Database.Insert(dto); + } + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index bba95792ee..d0d2abfe72 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -189,17 +189,19 @@ namespace Umbraco.Core.Services /// /// Id of the UserGroup to retrieve /// - IUserGroup GetUserGroupById(int id); - + IUserGroup GetUserGroupById(int id); + /// /// Saves a UserGroup /// /// UserGroup to save - /// Flag for whether to update the list of users in the group - /// List of user Ids + /// + /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in + /// than all users will be removed from this group and only these users will be added + /// /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events - void SaveUserGroup(IUserGroup userGroup, bool updateUsers = false, int[] userIds = null, bool raiseEvents = true); + void Save(IUserGroup userGroup, int[] userIds = null, bool raiseEvents = true); /// /// Deletes a UserGroup diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 07c0ad4e5b..8206ab4bca 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -682,11 +682,13 @@ namespace Umbraco.Core.Services /// Saves a UserGroup /// /// UserGroup to save - /// Flag for whether to update the list of users in the group - /// List of user Ids + /// + /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in + /// than all users will be removed from this group and only these users will be added + /// /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events - public void SaveUserGroup(IUserGroup userGroup, bool updateUsers = false, int[] userIds = null, bool raiseEvents = true) + public void Save(IUserGroup userGroup, int[] userIds = null, bool raiseEvents = true) { using (var uow = UowProvider.GetUnitOfWork()) { @@ -697,14 +699,8 @@ namespace Umbraco.Core.Services } var repository = RepositoryFactory.CreateUserGroupRepository(uow); - repository.AddOrUpdate(userGroup); + repository.AddOrUpdateGroupWithUsers(userGroup, userIds); - - if (updateUsers) - { - repository.RemoveAllUsersFromGroup(userGroup.Id); - repository.AddUsersToGroup(userGroup.Id, userIds); - } uow.Commit(); if (raiseEvents) diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 62b22f681a..fc2b870eef 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -339,16 +339,15 @@ namespace Umbraco.Tests.Persistence.Repositories private static User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository, IDatabaseUnitOfWork unitOfWork) { - var group = MockedUserGroup.CreateUserGroup(); - userGroupRepository.AddOrUpdate(@group); - unitOfWork.Commit(); - + var user = MockedUser.CreateUser(); repository.AddOrUpdate(user); unitOfWork.Commit(); - userGroupRepository.AddUsersToGroup(@group.Id, new[] { user.Id }); - unitOfWork.Commit(); + var group = MockedUserGroup.CreateUserGroup(); + userGroupRepository.AddOrUpdateGroupWithUsers(@group, new[] {user.Id}); + unitOfWork.Commit(); + return user; } diff --git a/src/Umbraco.Tests/Services/SectionServiceTests.cs b/src/Umbraco.Tests/Services/SectionServiceTests.cs index 556203bb61..0d95cfc56d 100644 --- a/src/Umbraco.Tests/Services/SectionServiceTests.cs +++ b/src/Umbraco.Tests/Services/SectionServiceTests.cs @@ -60,7 +60,8 @@ namespace Umbraco.Tests.Services }; userGroupA.AddAllowedSection("media"); userGroupA.AddAllowedSection("settings"); - ServiceContext.UserService.SaveUserGroup(userGroupA, true, new[] { user.Id }, false); + //TODO: This is failing the test + ServiceContext.UserService.Save(userGroupA, new[] { user.Id }, false); var userGroupB = new UserGroup { @@ -69,7 +70,7 @@ namespace Umbraco.Tests.Services }; userGroupB.AddAllowedSection("settings"); userGroupB.AddAllowedSection("developer"); - ServiceContext.UserService.SaveUserGroup(userGroupB, true, new[] { user.Id }, false); + ServiceContext.UserService.Save(userGroupB, new[] { user.Id }, false); return ServiceContext.UserService.GetUserById(user.Id); } diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index f5b9389939..02a4f335cc 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -415,7 +415,7 @@ namespace Umbraco.Tests.Services }; userGroup.AddAllowedSection("content"); userGroup.AddAllowedSection("mediat"); - ServiceContext.UserService.SaveUserGroup(userGroup); + ServiceContext.UserService.Save(userGroup); var result1 = ServiceContext.UserService.GetUserGroupById(userGroup.Id); @@ -426,7 +426,7 @@ namespace Umbraco.Tests.Services userGroup.AddAllowedSection("test2"); userGroup.AddAllowedSection("test3"); userGroup.AddAllowedSection("test4"); - ServiceContext.UserService.SaveUserGroup(userGroup); + ServiceContext.UserService.Save(userGroup); result1 = ServiceContext.UserService.GetUserGroupById(userGroup.Id); @@ -441,7 +441,7 @@ namespace Umbraco.Tests.Services //now just re-add a couple result1.AddAllowedSection("test3"); result1.AddAllowedSection("test4"); - ServiceContext.UserService.SaveUserGroup(result1); + ServiceContext.UserService.Save(result1); //assert //re-get @@ -464,14 +464,14 @@ namespace Umbraco.Tests.Services Alias = "Group2", Name = "Group 2" }; - ServiceContext.UserService.SaveUserGroup(userGroup1); - ServiceContext.UserService.SaveUserGroup(userGroup2); + ServiceContext.UserService.Save(userGroup1); + ServiceContext.UserService.Save(userGroup2); //adds some allowed sections userGroup1.AddAllowedSection("test"); userGroup2.AddAllowedSection("test"); - ServiceContext.UserService.SaveUserGroup(userGroup1); - ServiceContext.UserService.SaveUserGroup(userGroup2); + ServiceContext.UserService.Save(userGroup1); + ServiceContext.UserService.Save(userGroup2); //now clear the section from all users ServiceContext.UserService.DeleteSectionFromAllUserGroups("test"); @@ -504,9 +504,9 @@ namespace Umbraco.Tests.Services Alias = "Group3", Name = "Group 3" }; - ServiceContext.UserService.SaveUserGroup(userGroup1); - ServiceContext.UserService.SaveUserGroup(userGroup2); - ServiceContext.UserService.SaveUserGroup(userGroup3); + ServiceContext.UserService.Save(userGroup1); + ServiceContext.UserService.Save(userGroup2); + ServiceContext.UserService.Save(userGroup3); //now add the section to specific groups ServiceContext.UserService.AddSectionToAllUserGroups("test", userGroup1.Id, userGroup2.Id); @@ -641,7 +641,7 @@ namespace Umbraco.Tests.Services Name = "Test Group", Permissions = "ABCDEFGHIJ1234567".ToCharArray().Select(x => x.ToString()) }; - ServiceContext.UserService.SaveUserGroup(userGroup); + ServiceContext.UserService.Save(userGroup); ServiceContext.UserService.AddSectionToAllUserGroups("content", 1); ServiceContext.UserService.AddSectionToAllUserGroups("media", 1); return userGroup; diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml index 7af0512ed7..1cb5e32201 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml @@ -235,14 +235,6 @@ - -
User Groups
- /create/simple.ascx - - - - -
Macro
/Create/PartialView.ascx diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml index 3edb5fcedc..af2eece2ab 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml @@ -212,15 +212,7 @@ -
- -
User Groups
- /create/simple.ascx - - - - -
+
Macro
/Create/PartialView.ascx diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e8c92893fa..b6a5d30f49 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -456,7 +456,6 @@ ASPXCodeBehind - diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs index 98e87ce34b..8c96b2069c 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs @@ -24,12 +24,12 @@ function openUserGroups(id) { public override void Render(ref XmlTree tree) { - List userGroups = UserGroup.GetAllUserGroups(); - foreach (UserGroup userGroup in userGroups) + var userGroups = ApplicationContext.Current.Services.UserService.GetAllUserGroups(); + foreach (var userGroup in userGroups) { XmlTreeNode node = XmlTreeNode.Create(this); node.NodeID = userGroup.Id.ToString(); - node.Action = string.Format("javascript:openUserGroups({0})", userGroup.Id.ToString()); + node.Action = string.Format("javascript:openUserGroups({0})", userGroup.Id); node.Icon = "icon-users"; node.Text = userGroup.Name; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs index 9b929bd931..a992c4d77e 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Web.UI.WebControls; using umbraco.BasePages; using umbraco.BusinessLogic; using umbraco.cms.presentation.Trees; using umbraco.interfaces; using Umbraco.Core; +using Umbraco.Core.Models.Membership; namespace umbraco.cms.presentation.user { @@ -65,7 +68,7 @@ namespace umbraco.cms.presentation.user actions += li.Value; } - userGroup.DefaultPermissions = actions; + userGroup.Permissions = actions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); ; var userIds = new List(); foreach (ListItem li in lstUsersInGroup.Items) @@ -73,13 +76,13 @@ namespace umbraco.cms.presentation.user userIds.Add(int.Parse(li.Value)); } - userGroup.ClearApplications(); + userGroup.ClearAllowedSections(); foreach (ListItem li in cbl_sections.Items) { - if (li.Selected) userGroup.AddApplication(li.Value); + if (li.Selected) userGroup.AddAllowedSection(li.Value); } - userGroup.SaveWithUsers(userIds.ToArray()); + Services.UserService.Save(userGroup, userIds.ToArray()); ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "editUserGroupSaved", base.getUser()), ""); } @@ -89,22 +92,22 @@ namespace umbraco.cms.presentation.user get { if (m_userGroupActions == null) - m_userGroupActions = umbraco.BusinessLogic.Actions.Action.FromString(CurrentUserGroup.DefaultPermissions); + m_userGroupActions = umbraco.BusinessLogic.Actions.Action.FromString(string.Join("", CurrentUserGroup.Permissions)); return m_userGroupActions; } } - protected UserGroup CurrentUserGroup + protected IUserGroup CurrentUserGroup { get { if (m_userGroup == null) - m_userGroup = UserGroup.GetUserGroup(m_userGroupID); + m_userGroup = ApplicationContext.Current.Services.UserService.GetUserGroupById(m_userGroupID); return m_userGroup; } } - private UserGroup m_userGroup; + private IUserGroup m_userGroup; private List m_userGroupActions; private int m_userGroupID; @@ -135,13 +138,17 @@ namespace umbraco.cms.presentation.user foreach (Application a in CurrentUser.Applications) currentUserApps += a.alias + ";"; - Application[] gapps = CurrentUserGroup.Applications; + var gapps = CurrentUserGroup.AllowedSections; foreach (Application app in BusinessLogic.Application.getAll()) { if (CurrentUser.IsAdmin() || currentUserApps.Contains(";" + app.alias + ";")) { ListItem li = new ListItem(ui.Text("sections", app.alias), app.alias); - foreach (Application tmp in gapps) if (app.alias == tmp.alias) li.Selected = true; + foreach (var tmp in gapps) + { + if (app.alias == tmp) + li.Selected = true; + } cbl_sections.Items.Add(li); } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupTasks.cs deleted file mode 100644 index 0aae75d278..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupTasks.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Umbraco.Web.UI; -using umbraco.BusinessLogic; - -namespace umbraco.cms.presentation.user -{ - public class UserGroupTasks : LegacyDialogTask - { - public override bool PerformSave() - { - try - { - var u = UserGroup.MakeNew(Alias, "", Alias); - _returnUrl = string.Format("users/EditUserGroup.aspx?id={0}", u.Id); - return true; - } - catch - { - return false; - } - } - - public override bool PerformDelete() - { - var userGroup = UserGroup.GetUserGroup(ParentID); - if (userGroup == null) - return false; - userGroup.Delete(); - return true; - } - - private string _returnUrl = ""; - public override string ReturnUrl - { - get { return _returnUrl; } - } - - public override string AssignedApp - { - get { return DefaultApps.users.ToString(); } - } - } -} \ No newline at end of file diff --git a/src/umbraco.businesslogic/UserGroup.cs b/src/umbraco.businesslogic/UserGroup.cs deleted file mode 100644 index 4a18a95c33..0000000000 --- a/src/umbraco.businesslogic/UserGroup.cs +++ /dev/null @@ -1,309 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Events; - -namespace umbraco.BusinessLogic -{ - /// - /// Represents a umbraco Usergroup - /// - [Obsolete("Use the UserService instead")] - public class UserGroup - { - internal Umbraco.Core.Models.Membership.IUserGroup UserGroupItem; - - /// - /// Creates a new empty instance of a UserGroup - /// - public UserGroup() - { - UserGroupItem = new Umbraco.Core.Models.Membership.UserGroup(); - } - - internal UserGroup(Umbraco.Core.Models.Membership.IUserGroup userGroup) - { - UserGroupItem = userGroup; - } - - /// - /// Creates a new instance of a UserGroup and attempts to - /// load it's values from the database cache. - /// - /// - /// If the UserGroup is not found in the existing ID list, then this object - /// will remain an empty object - /// - /// The UserGroup id to find - public UserGroup(int id) - { - this.LoadByPrimaryKey(id); - } - - /// - /// Initializes a new instance of the class. - /// - /// The user type id. - /// The name. - public UserGroup(int id, string name) - { - UserGroupItem = new Umbraco.Core.Models.Membership.UserGroup(); - UserGroupItem.Id = id; - UserGroupItem.Name = name; - } - - /// - /// Creates a new instance of UserGroup with all parameters - /// - /// - /// - /// - /// - public UserGroup(int id, string name, string defaultPermissions, string alias) - { - UserGroupItem = new Umbraco.Core.Models.Membership.UserGroup(); - UserGroupItem.Id = id; - UserGroupItem.Name = name; - UserGroupItem.Alias = alias; - UserGroupItem.Permissions = defaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// The cache storage for all user groups - /// - private static List UserGroups - { - get - { - return ApplicationContext.Current.Services.UserService.GetAllUserGroups() - .Select(x => new UserGroup(x)) - .ToList(); - } - } - - #region Public Properties - /// - /// Gets or sets the user type alias. - /// - public string Alias - { - get { return UserGroupItem.Alias; } - set { UserGroupItem.Alias = value; } - } - - /// - /// Gets the name of the user type. - /// - public string Name - { - get { return UserGroupItem.Name; } - set { UserGroupItem.Name = value; } - } - - /// - /// Gets the id the user group - /// - public int Id - { - get { return UserGroupItem.Id; } - } - - /// - /// Gets the default permissions of the user group - /// - public string DefaultPermissions - { - get { return UserGroupItem.Permissions == null ? string.Empty : string.Join("", UserGroupItem.Permissions); } - set { UserGroupItem.Permissions = value.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); } - } - - /// - /// Gets the applications which the group has access to. - /// - /// The users applications. - public Application[] Applications - { - get - { - return GetApplications().ToArray(); - } - } - - /// - /// Get the application which the group has access to as a List - /// - /// - public List GetApplications() - { - var allApps = Application.getAll(); - var apps = new List(); - - var sections = UserGroupItem.AllowedSections; - - foreach (var s in sections) - { - var app = allApps.SingleOrDefault(x => x.alias == s); - if (app != null) - apps.Add(app); - } - - return apps; - } - - /// - /// Clears the list of applications the user has access to, ensure to call Save afterwords - /// - public void ClearApplications() - { - foreach (var s in UserGroupItem.AllowedSections.ToArray()) - { - UserGroupItem.RemoveAllowedSection(s); - } - } - - /// - /// Adds a application to the list of allowed applications, ensure to call Save() afterwords - /// - /// - public void AddApplication(string appAlias) - { - UserGroupItem.AddAllowedSection(appAlias); - } - - #endregion - - /// - /// Saves this instance - /// - public void Save() - { - PerformSave(); - } - - /// - /// Saves this instance (along with the list of users in the group if provided) - /// - public void SaveWithUsers(int[] userIds) - { - PerformSave(true, userIds); - } - - private void PerformSave(bool updateUsers = false, int[] userIds = null) - { - //ensure that this object has an ID specified (it exists in the database) - if (UserGroupItem.HasIdentity == false) - throw new Exception("The current UserGroup object does not exist in the database. New UserGroups should be created with the MakeNew method"); - - ApplicationContext.Current.Services.UserService.SaveUserGroup(UserGroupItem, updateUsers, userIds); - - //raise event - OnUpdated(this, new EventArgs()); - } - - /// - /// Deletes this instance. - /// - public void Delete() - { - //ensure that this object has an ID specified (it exists in the database) - if (UserGroupItem.HasIdentity == false) - throw new Exception("The current UserGroup object does not exist in the database. New UserGroups should be created with the MakeNew method"); - - ApplicationContext.Current.Services.UserService.DeleteUserGroup(UserGroupItem); - - //raise event - OnDeleted(this, new EventArgs()); - } - - /// - /// Load the data for the current UserGroup by it's id - /// - /// - /// Returns true if the UserGroup id was found - /// and the data was loaded, false if it wasn't - public bool LoadByPrimaryKey(int id) - { - UserGroupItem = ApplicationContext.Current.Services.UserService.GetUserGroupById(id); - return UserGroupItem != null; - } - - /// - /// Creates a new user group - /// - /// - /// - /// - public static UserGroup MakeNew(string name, string defaultPermissions, string alias) - { - //ensure that the current alias does not exist - //get the id for the new user group - var existing = UserGroups.Find(ut => (ut.Alias == alias)); - - if (existing != null) - throw new Exception("The UserGroup alias specified already exists"); - - var userType = new Umbraco.Core.Models.Membership.UserGroup - { - Alias = alias, - Name = name, - Permissions = defaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)) - }; - ApplicationContext.Current.Services.UserService.SaveUserGroup(userType); - - var legacy = new UserGroup(userType); - - //raise event - OnNew(legacy, new EventArgs()); - - return legacy; - } - - /// - /// Gets the user group with the specfied ID - /// - /// The id. - /// - public static UserGroup GetUserGroup(int id) - { - return UserGroups.Find(ut => (ut.Id == id)); - } - - /// - /// Returns all user groups - /// - /// - public static List GetAllUserGroups() - { - return UserGroups; - } - - internal static event TypedEventHandler New; - private static void OnNew(UserGroup userType, EventArgs args) - { - if (New != null) - { - New(userType, args); - } - } - - internal static event TypedEventHandler Deleted; - private static void OnDeleted(UserGroup userType, EventArgs args) - { - if (Deleted != null) - { - Deleted(userType, args); - } - } - - internal static event TypedEventHandler Updated; - private static void OnUpdated(UserGroup userType, EventArgs args) - { - if (Updated != null) - { - Updated(userType, args); - } - } - } -} \ No newline at end of file diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index 299742b1fc..8e2d3f4dd6 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -242,7 +242,6 @@ Code - Code From ffd63a9103684ac696fb20171f19179dd40f6ef9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 May 2017 15:51:30 +1000 Subject: [PATCH 057/714] fixes merge issues --- src/Umbraco.Web/Editors/UserController.cs | 4 ++-- src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs | 5 +++-- src/Umbraco.Web/Models/Mapping/UserModelMapper.cs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Editors/UserController.cs b/src/Umbraco.Web/Editors/UserController.cs index 1cdb243742..b0da9a3f47 100644 --- a/src/Umbraco.Web/Editors/UserController.cs +++ b/src/Umbraco.Web/Editors/UserController.cs @@ -68,14 +68,14 @@ namespace Umbraco.Web.Editors var startId = 100 + ((pageNumber -1) * pageSize); var numUsers = pageSize; var users = new List(); - var userTypes = Services.UserService.GetAllUserTypes().ToDictionary(x => x.Alias, x => x.Name); + var userTypes = Services.UserService.GetAllUserGroups().ToDictionary(x => x.Alias, x => x.Name); var cultures = Services.TextService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); for (int i = 0; i < numUsers; i++) { var display = new UserDisplay { Id = startId, - UserType = "writer", + //UserType = "writer", AllowedSections = new[] {"content", "media"}, AvailableUserTypes = userTypes, Email = "test" + startId + "@test.com", diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index d77d9d2019..76a480c80d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -21,8 +21,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "email", IsRequired = true)] public string Email { get; set; } - [DataMember(Name = "userType")] - public string UserType { get; set; } + //TODO: Should be user groups! + //[DataMember(Name = "userType")] + //public string UserType { get; set; } /// /// Gets the available user types (i.e. to populate a drop down) diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index f6cad9169d..ea634ac09a 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.Models.Mapping { config.CreateMap() .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) - .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) + //.ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) From 26b8236eead4d6f0f373753718e4f8a8ff0e3e53 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 May 2017 16:09:01 +1000 Subject: [PATCH 058/714] fixes user query relator --- src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs b/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs index 7a52f710a4..0d9b1d5552 100644 --- a/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs +++ b/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs @@ -62,13 +62,17 @@ namespace Umbraco.Core.Persistence.Relators //add the current (new) group _currentUser.UserGroupDtos.Add(group); } - + AddSection(section); } } private void AddSection(UserGroup2AppDto section) { + //this can be null since we are left joining + if (section == null || section.AppAlias.IsNullOrWhiteSpace()) + return; + var latestGroup = _currentUser.UserGroupDtos.Count > 0 ? _currentUser.UserGroupDtos[_currentUser.UserGroupDtos.Count - 1] : null; From 4f80902ac2a395c45a38731144f36bf029c02289 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 May 2017 17:17:02 +1000 Subject: [PATCH 059/714] Fixes unit tests - all but one --- src/Umbraco.Core/Models/Membership/User.cs | 1 + .../Persistence/Repositories/UserGroupRepository.cs | 9 +++++++++ src/Umbraco.Core/TypeFinder.cs | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index e1b6a09cae..fe9bd7d152 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -31,6 +31,7 @@ namespace Umbraco.Core.Models.Membership _startMediaId = -1; //cannot be null _rawPasswordValue = ""; + _allowedSections = new List(); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index 2cc8819da0..58a4e90b5d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -281,6 +281,15 @@ namespace Umbraco.Core.Persistence.Repositories UserIds = userIds; } + public override bool HasIdentity + { + get { return UserGroup.HasIdentity; } + protected set + { + throw new NotSupportedException(); + } + } + public IUserGroup UserGroup { get; private set; } public int[] UserIds { get; private set; } diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index 2af6e99cc7..ab2b24392e 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -7,6 +7,7 @@ using System.Security; using System.Text; using System.Web; using System.Web.Compilation; +using System.Web.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -44,7 +45,7 @@ namespace Umbraco.Core HashSet assemblies = null; try { - var isHosted = HttpContext.Current != null; + var isHosted = HttpContext.Current != null || HostingEnvironment.IsHosted; try { From 225f657154510ee3b9ff91cc1941fdf4bb94c96e Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 May 2017 18:18:10 +1000 Subject: [PATCH 060/714] fixes tests for mappings --- src/Umbraco.Web/Editors/UserController.cs | 2 +- .../Models/ContentEditing/UserDisplay.cs | 29 ++++++++++++++----- .../Models/Mapping/UserModelMapper.cs | 21 ++++++++++++-- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web/Editors/UserController.cs b/src/Umbraco.Web/Editors/UserController.cs index b0da9a3f47..7f24f93d4c 100644 --- a/src/Umbraco.Web/Editors/UserController.cs +++ b/src/Umbraco.Web/Editors/UserController.cs @@ -77,7 +77,7 @@ namespace Umbraco.Web.Editors Id = startId, //UserType = "writer", AllowedSections = new[] {"content", "media"}, - AvailableUserTypes = userTypes, + AvailableUserGroups = userTypes, Email = "test" + startId + "@test.com", Name = "User " + startId, Culture = "en-US", diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index 76a480c80d..1385e395ec 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Runtime.Serialization; @@ -20,23 +21,27 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "email", IsRequired = true)] public string Email { get; set; } - - //TODO: Should be user groups! - //[DataMember(Name = "userType")] - //public string UserType { get; set; } + + /// + /// The list of group aliases assigned to the user + /// + [DataMember(Name = "userGroups")] + public IEnumerable UserGroups { get; set; } /// - /// Gets the available user types (i.e. to populate a drop down) - /// The key is the Alias the value is the Name - the Alias is what is used in the UserType property and for persistence + /// Gets the available user groups (i.e. to populate a drop down) + /// The key is the Alias the value is the Name - the Alias is what is used in the UserGroup property and for persistence /// - [DataMember(Name = "availableUserTypes")] - public IDictionary AvailableUserTypes { get; set; } + [DataMember(Name = "availableUserGroups")] + [ReadOnly(true)] + public IDictionary AvailableUserGroups { get; set; } /// /// Gets the available cultures (i.e. to populate a drop down) /// The key is the culture stored in the database, the value is the Name /// [DataMember(Name = "availableCultures")] + [ReadOnly(true)] public IDictionary AvailableCultures { get; set; } [DataMember(Name = "startContentId")] @@ -51,6 +56,14 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowedSections")] public IEnumerable AllowedSections { get; set; } + /// + /// Gets the available sections (i.e. to populate a drop down) + /// The key is the Alias the value is the Name - the Alias is what is used in the AllowedSections property and for persistence + /// + [DataMember(Name = "availableSections")] + [ReadOnly(true)] + public IDictionary AvailableSections { get; set; } + /// /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. /// diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index ea634ac09a..f58dfe875a 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -18,13 +18,28 @@ namespace Umbraco.Web.Models.Mapping { config.CreateMap() .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) - //.ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) + .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember( - detail => detail.AvailableUserTypes, - opt => opt.MapFrom(user => applicationContext.Services.SectionService.GetSections().ToDictionary(x => x.Alias, x => x.Name))); + detail => detail.AvailableUserGroups, + opt => opt.MapFrom(user => applicationContext.Services.UserService.GetAllUserGroups().ToDictionary(x => x.Alias, x => x.Name))) + .ForMember( + detail => detail.AvailableSections, + opt => opt.MapFrom(user => applicationContext.Services.SectionService.GetSections().ToDictionary(x => x.Alias, x => x.Name))) + .ForMember( + detail => detail.AvailableCultures, + opt => opt.MapFrom(user => applicationContext.Services.TextService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName))) + .ForMember(detail => detail.ParentId, opt => opt.UseValue(-1)) + .ForMember(detail => detail.Path, opt => opt.MapFrom(user => "-1," + user.Id)) + .ForMember(detail => detail.Notifications, opt => opt.Ignore()) + .ForMember(detail => detail.Udi, opt => opt.Ignore()) + .ForMember(detail => detail.Icon, opt => opt.Ignore()) + .ForMember(detail => detail.Trashed, opt => opt.Ignore()) + .ForMember(detail => detail.Alias, opt => opt.Ignore()) + .ForMember(detail => detail.Trashed, opt => opt.Ignore()) + .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()); config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => GetIntId(user.Id))) From 73d7915fb5cabc674ea810d20078c54a4f5a9937 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 16 May 2017 13:14:29 +0200 Subject: [PATCH 061/714] fixes: U4-9900 Add support for selecting an editor sub view trough route params --- .../src/views/users/overview.controller.js | 10 ++++++---- .../src/views/users/role.controller.js | 2 +- .../src/views/users/user.controller.js | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js index ffee2a6775..e0acfbc30a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/overview.controller.js @@ -1,9 +1,10 @@ (function () { "use strict"; - function UsersOverviewController($scope) { + function UsersOverviewController($scope, $location) { var vm = this; + var usersUri = $location.search().subview; vm.page = {}; vm.page.name = "User Management"; @@ -12,17 +13,18 @@ "name": "Users", "icon": "icon-user", "view": "views/users/views/users/users.html", - "active": true + "active": !usersUri || usersUri === "users" }, { "name": "Roles", "icon": "icon-users", - "view": "views/users/views/roles/roles.html" + "view": "views/users/views/roles/roles.html", + "active": usersUri === "roles" } ]; function init() { - + } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js index 3381fd81a1..12cb81e08d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js @@ -28,7 +28,7 @@ } function goBack() { - $location.path("users/users/overview"); + $location.path("/users/users/overview").search("subview", "roles"); } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 09e74c5455..fd3950bde6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -28,7 +28,7 @@ } function goBack() { - $location.path("users/users/overview"); + $location.path("/users/users/overview").search("subview", "users"); } init(); From c299edfe5a522834447b7d3916493f44d0ce1b23 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 16 May 2017 15:08:49 +0200 Subject: [PATCH 062/714] U4-9901 User role picker should show permission details --- .../users/umbuserrolepreview.directive.js | 33 ++++++++ .../src/common/resources/users.resource.js | 20 ++++- src/Umbraco.Web.UI.Client/src/less/belle.less | 2 + .../users/umb-user-role-picker-list.less | 38 +++++++++ .../users/umb-user-role-preview.less | 64 +++++++++++++++ .../userrolepicker.controller.js | 77 +++++++++++++++++++ .../userrolepicker/userrolepicker.html | 67 ++++++++++++++++ .../users/umb-user-role-preview.html | 38 +++++++++ .../users/views/users/users.controller.js | 30 ++++---- .../src/views/users/views/users/users.html | 18 +++-- 10 files changed, 361 insertions(+), 26 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbuserrolepreview.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-picker-list.less create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-preview.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/overlays/userrolepicker/userrolepicker.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/overlays/userrolepicker/userrolepicker.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-role-preview.html diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbuserrolepreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbuserrolepreview.directive.js new file mode 100644 index 0000000000..c2513e8aae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbuserrolepreview.directive.js @@ -0,0 +1,33 @@ +(function () { + 'use strict'; + + function UserRolePreviewDirective() { + + function link(scope, el, attr, ctrl) { + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/users/umb-user-role-preview.html', + scope: { + icon: "=?", + name: "=", + description: "=?", + sections: "=?", + contentStartNodes: "=?", + mediaStartNodes: "=?", + allowRemove: "=?", + onRemove: "&?" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbUserRolePreview', UserRolePreviewDirective); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 3e4764b45b..dbb07efca9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -247,25 +247,37 @@ "name": "Admin", "alias": "admin", "id": 1, - "icon": "icon-medal" + "icon": "icon-medal", + "sections": [], + "startNodesContent": [], + "startNodesMedia": [] }, { "name": "Writer", "alias": "writer", "id": 2, - "icon": "icon-edit" + "icon": "icon-edit", + "sections": [{ "id": "1", "name": "Content"}, { "id": "2", "name": "Media"}], + "startNodesContent": [], + "startNodesMedia": [] }, { "name": "Editor", "alias": "editor", "id": 3, - "icon": "icon-tools" + "icon": "icon-tools", + "sections": [{ "id": "1", "name": "Content"}, { "id": "2", "name": "Media"}], + "startNodesContent": [{ "id": "1", "name": "Start node 1"}, { "id": "2", "name": "Start node 2"}], + "startNodesMedia": [{ "id": "1", "name": "Start node 1"}, { "id": "2", "name": "Start node 2"}] }, { "name": "Translator", "alias": "translator", "id": 4, - "icon": "icon-globe" + "icon": "icon-globe", + "sections": [{ "id": "1", "name": "Content"}, { "id": "2", "name": "Translations"}], + "startNodesContent": [{ "id": "1", "name": "Start node 1"}], + "startNodesMedia": [{ "id": "1", "name": "Start node 1"}] } ]; deferred.resolve(userGroups); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index ae4a8bf190..887b9e7816 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -133,6 +133,8 @@ @import "components/umb-mini-editor.less"; @import "components/users/umb-users.less"; +@import "components/users/umb-user-role-picker-list.less"; +@import "components/users/umb-user-role-preview.less"; // Utilities @import "utilities/_flexbox.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-picker-list.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-picker-list.less new file mode 100644 index 0000000000..5119b5d536 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-picker-list.less @@ -0,0 +1,38 @@ +.umb-user-role-picker-list { + display: flex; + flex-direction: column; +} + +.umb-user-role-picker-list-item { + display: flex; + margin-bottom: 5px; + padding: 10px; +} + +.umb-user-role-picker-list-item:active, +.umb-user-role-picker-list-item:focus { + text-decoration: none; +} + +.umb-user-role-picker-list-item:hover { + background-color: @gray-10; + text-decoration: none; +} + +.umb-user-role-picker-list-item__icon { + font-size: 20px; + line-height: 20px; + min-width: 20px; + margin-right: 15px; +} + +.umb-user-role-picker-list-item__name { + font-size: 15px; + margin-bottom: 3px; + font-weight: bold; +} + +.umb-user-role-picker-list-item__permission { + font-size: 13px; + color: @gray-4; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-preview.less new file mode 100644 index 0000000000..a772a8ff28 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-preview.less @@ -0,0 +1,64 @@ +.umb-user-role-preview { + padding-top: 10px; + padding-bottom: 10px; + display: flex; + box-sizing: border-box; + border-bottom: 1px solid @gray-9; +} + +.umb-user-role-preview:last-of-type { + border-bottom: none; + margin-bottom: 5px; +} + +.umb-user-role-preview__icon { + display: flex; + width: 25px; + height: 25px; + justify-content: center; + align-items: center; + font-size: 20px; + margin-right: 10px; + flex: 0 0 auto; +} + +.umb-user-role-preview__content { + flex: 1 1 auto; +} + +.umb-user-role-preview__name { + font-size: 15px; + font-weight: bold; + color: @black; + margin-bottom: 3px; + margin-top: 2px; +} + +.umb-user-role-preview__permission { + font-size: 13px; + color: @gray-3; +} + +.umb-user-role-preview__actions { + flex: 0 0 auto; + display: flex; + align-items: center; +} + +.umb-user-role-preview__action { + margin-left: 5px; + margin-right: 5px; + font-size: 13px; + font-weight: bold; + color: @gray-5; +} + +.umb-user-role-preview__action:hover { + color: @turquoise; + text-decoration: none; + opacity: 1; +} + +.umb-user-role-preview__action--red:hover { + color: @red; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/userrolepicker/userrolepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/userrolepicker/userrolepicker.controller.js new file mode 100644 index 0000000000..bace5a16f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/userrolepicker/userrolepicker.controller.js @@ -0,0 +1,77 @@ +(function () { + "use strict"; + + function UserRolePickerController($scope, usersResource) { + + var vm = this; + + vm.userRoles = []; + vm.loading = false; + + vm.selectUserRole = selectUserRole; + + ////////// + + function onInit() { + + vm.loading = true; + + // make sure we can push to something + if(!$scope.model.selection) { + $scope.model.selection = []; + } + + // get venues + usersResource.getUserGroups().then(function(userRoles){ + vm.userRoles = userRoles; + + if($scope.model.selection && $scope.model.selection.length > 0) { + preSelect($scope.model.selection); + } + + vm.loading = false; + + }); + + } + + function preSelect(selection) { + + angular.forEach(selection, function(selected){ + + angular.forEach(vm.userRoles, function(userRole){ + if(selected.id === userRole.id) { + userRole.selected = true; + } + }); + + }); + } + + function selectUserRole(userRole) { + + if(!userRole.selected) { + + userRole.selected = true; + $scope.model.selection.push(userRole); + + } else { + + angular.forEach($scope.model.selection, function(selectedUserRole, index){ + if(selectedUserRole.id === userRole.id) { + userRole.selected = false; + $scope.model.selection.splice(index, 1); + } + }); + + } + + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Overlays.UserRolePickerController", UserRolePickerController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/userrolepicker/userrolepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/userrolepicker/userrolepicker.html new file mode 100644 index 0000000000..25cd1ae315 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/userrolepicker/userrolepicker.html @@ -0,0 +1,67 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-role-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-role-preview.html new file mode 100644 index 0000000000..b4682d35b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-role-preview.html @@ -0,0 +1,38 @@ +
+ + + +
+
{{ name }}
+ +
+ + Sections: + {{ section.name }}, + All sections + +
+ +
+ + Content start node: + {{ startNode.name }}, + Content root + +
+ +
+ + Media start node: + {{ startNode.name }}, + Media root + +
+ +
+ +
+ Remove +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index cd96c3893c..d1dfb2792e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -147,25 +147,27 @@ } function openUserGroupPicker(event) { - vm.userGroupPicker = { - title: "Select user groups", - view: "itempicker", - event: event, - availableItems: vm.userGroups, - selectedItems: vm.newUser.userGroups, + vm.userRolePicker = { + title: "Select user roles", + view: "userrolepicker", + selection: vm.newUser.userGroups, + closeButtonLabel: "Cancel", show: true, submit: function(model) { - - if(model.selectedItem) { - vm.newUser.userGroups.push(model.selectedItem); + // apply changes + if(model.selection) { + vm.newUser.userGroups = model.selection; } - - vm.userGroupPicker.show = false; - vm.userGroupPicker = null; + vm.userRolePicker.show = false; + vm.userRolePicker = null; }, close: function(oldModel) { - vm.userGroupPicker.show = false; - vm.userGroupPicker = null; + // rollback on close + if(oldModel.selection) { + vm.newUser.userGroups = oldModel.selection; + } + vm.userRolePicker.show = false; + vm.userRolePicker = null; } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 472e8747f8..5f0ad74e29 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -154,7 +154,7 @@ From 9bb880f1954b49f44e919f0cdd7b74eb9ba0348e Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 May 2017 19:47:16 +1000 Subject: [PATCH 063/714] Starts getting the user service/repo to do paging filtering nicely --- src/Umbraco.Core/Models/Membership/IUser.cs | 2 + src/Umbraco.Core/Models/Membership/User.cs | 13 ++ .../Models/Membership/UserState.cs | 13 ++ .../Interfaces/IUserRepository.cs | 14 +- .../Repositories/UserRepository.cs | 205 ++++++++++++++++-- src/Umbraco.Core/Services/IUserService.cs | 5 + src/Umbraco.Core/Services/UserService.cs | 53 ++++- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Cache/UserGroupCacheRefresher.cs | 1 + 9 files changed, 280 insertions(+), 27 deletions(-) create mode 100644 src/Umbraco.Core/Models/Membership/UserState.cs diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index c4ccb6328b..edb9d9aa7d 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -9,6 +9,8 @@ namespace Umbraco.Core.Models.Membership /// Will be left internal until a proper Membership implementation is part of the roadmap public interface IUser : IMembershipUser, IRememberBeingDirty, ICanBeDirty { + UserState UserState { get; } + string Name { get; set; } int SessionTimeout { get; set; } int StartContentId { get; set; } diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index fe9bd7d152..055b3e439d 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -225,6 +225,19 @@ namespace Umbraco.Core.Models.Membership #region Implementation of IUser + public UserState UserState + { + get + { + if (IsLockedOut) + return UserState.LockedOut; + if (IsApproved == false) + return UserState.Disabled; + //TODO: Fill in the invite details + return UserState.Active; + } + } + [DataMember] public string Name { diff --git a/src/Umbraco.Core/Models/Membership/UserState.cs b/src/Umbraco.Core/Models/Membership/UserState.cs new file mode 100644 index 0000000000..23a8f2a2a4 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserState.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models.Membership +{ + /// + /// The state of a user + /// + public enum UserState + { + Active = 0, + Disabled = 1, + LockedOut = 2, + Invited = 3 + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs index 64d770e64f..59335daac0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq.Expressions; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories @@ -34,15 +36,23 @@ namespace Umbraco.Core.Persistence.Repositories /// Id of group IEnumerable GetAllNotInGroup(int groupId); + [Obsolete("Use the overload with long operators instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, Expression> orderBy); + /// - /// Gets paged member results + /// Gets paged user results /// /// /// /// /// /// + /// + /// + /// Optional parameter to filter by specified user groups + /// Optional parameter to filter by specfied user state /// - IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, Expression> orderBy); + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Expression> orderBy, Direction orderDirection, string[] userGroups = null, UserState? userState = null, IQuery filter = null); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 703b7dc639..bcf1ca26a8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; @@ -316,24 +318,45 @@ namespace Umbraco.Core.Persistence.Repositories return ConvertFromDtos(Database.Fetch(sql)); } + [Obsolete("Use the overload with long operators instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, Expression> orderBy) + { + if (orderBy == null) throw new ArgumentNullException("orderBy"); + + // get the referenced column name and find the corresp mapped column name + var expressionMember = ExpressionHelper.GetMemberInfo(orderBy); + var mapper = MappingResolver.Current.ResolveMapperByType(typeof(IUser)); + var mappedField = mapper.Map(expressionMember.Name); + + if (mappedField.IsNullOrWhiteSpace()) + throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); + + long tr; + var results = GetPagedResultsByQuery(query, Convert.ToInt64(pageIndex), pageSize, out tr, mappedField, Direction.Ascending); + totalRecords = Convert.ToInt32(tr); + return results; + } + /// /// Gets paged user results /// - /// - /// The where clause, if this is null all records are queried - /// + /// /// /// /// /// + /// + /// Optional parameter to filter by specified user groups + /// Optional parameter to filter by specfied user state + /// /// /// /// The query supplied will ONLY work with data specifically on the umbracoUser table because we are using PetaPoco paging (SQL paging) /// - public IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, Expression> orderBy) + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Expression> orderBy, Direction orderDirection, string[] userGroups = null, UserState? userState = null, IQuery filter = null) { - if (orderBy == null) - throw new ArgumentNullException("orderBy"); + if (orderBy == null) throw new ArgumentNullException("orderBy"); // get the referenced column name and find the corresp mapped column name var expressionMember = ExpressionHelper.GetMemberInfo(orderBy); @@ -343,29 +366,163 @@ namespace Umbraco.Core.Persistence.Repositories if (mappedField.IsNullOrWhiteSpace()) throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); - var sql = new Sql() - .Select("umbracoUser.Id") - .From(SqlSyntax); - - var idsQuery = query == null ? sql : new SqlTranslator(sql, query).Translate(); - - // need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177 - idsQuery.OrderBy("(" + mappedField + ")"); - var page = Database.Page(pageIndex + 1, pageSize, idsQuery); - totalRecords = Convert.ToInt32(page.TotalItems); - - if (totalRecords == 0) - return Enumerable.Empty(); - - // now get the actual users and ensure they are ordered properly (same clause) - var ids = page.Items.ToArray(); - return ids.Length == 0 ? Enumerable.Empty() : GetAll(ids).OrderBy(orderBy.Compile()); + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, mappedField, orderDirection, userGroups, userState, filter); } + private IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, string[] userGroups = null, UserState? userState = null, IQuery filter = null) + { + if (string.IsNullOrWhiteSpace(orderBy)) throw new ArgumentException("Value cannot be null or whitespace.", "orderBy"); + + + var filterSql = new Sql(); + if (filter != null) + { + foreach (var filterClaus in filter.GetWhereClauses()) + { + filterSql.Append(string.Format("AND ({0})", filterClaus.Item1), filterClaus.Item2); + } + } + Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); + + // Get base query for returning IDs + var sqlBaseIds = GetBaseQuery("id"); + // Get base query for returning all data + var sqlBaseFull = GetBaseQuery(false); + + if (query == null) query = new Query(); + var translatorIds = new SqlTranslator(sqlBaseIds, query); + var sqlQueryIds = translatorIds.Translate(); + var translatorFull = new SqlTranslator(sqlBaseFull, query); + var sqlQueryFull = translatorFull.Translate(); + + //get sorted and filtered sql + var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( + GetFilteredSqlForPagedResults(sqlQueryIds, filterCallback), + orderDirection, orderBy); + + // Get page of results and total count + IEnumerable result; + var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + return Enumerable.Empty(); + + ////NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number + //// that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in + //// the pageResult, then the GetAll will actually return ALL records in the db. + //if (pagedResult.Items.Any()) + //{ + // //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + // var args = sqlNodeIdsWithSort.Arguments; + // string sqlStringCount, sqlStringPage; + // Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); + + // //We need to make this FULL query an inner join on the paged ID query + // var splitQuery = sqlQueryFull.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); + // var fullQueryWithPagedInnerJoin = new Sql(splitQuery[0]) + // .Append("INNER JOIN (") + // //join the paged query with the paged query arguments + // .Append(sqlStringPage, args) + // .Append(") temp ") + // .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) + // //add the original where clause back with the original arguments + // .Where(splitQuery[1], sqlQueryIds.Arguments); + + // //get sorted and filtered sql + // var fullQuery = GetSortedSqlForPagedResults( + // GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, defaultFilter), + // orderDirection, orderBy, orderBySystemField, nodeIdSelect); + + // return processQuery(fullQuery, new PagingSqlQuery(Database, sqlNodeIdsWithSort, pageIndex, pageSize)); + //} + //else + //{ + // result = Enumerable.Empty(); + //} + + //return result; + + + //return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + // new Tuple("cmsMember", "nodeId"), + // (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, + // filterCallback); + + + //var sql = new Sql() + // .Select("umbracoUser.Id") + // .From(SqlSyntax); + + //var idsQuery = query == null ? sql : new SqlTranslator(sql, query).Translate(); + + //// need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177 + //idsQuery.OrderBy("(" + orderBy + ")"); + //var page = Database.Page(pageIndex + 1, pageSize, idsQuery); + //totalRecords = Convert.ToInt32(page.TotalItems); + + //if (totalRecords == 0) + // return Enumerable.Empty(); + + //// now get the actual users and ensure they are ordered properly (same clause) + //var ids = page.Items.ToArray(); + //return ids.Length == 0 ? Enumerable.Empty() : GetAll(ids).OrderBy(orderBy.Compile()); + } + + private Sql GetFilteredSqlForPagedResults(Sql sql, Func> defaultFilter = null) + { + Sql filteredSql; + + // Apply filter + if (defaultFilter != null) + { + var filterResult = defaultFilter(); + + //NOTE: this is certainly strange - NPoco handles this much better but we need to re-create the sql + // instance a couple of times to get the parameter order correct, for some reason the first + // time the arguments don't show up correctly but the SQL argument parameter names are actually updated + // accordingly - so we re-create it again. In v8 we don't need to do this and it's already taken care of. + + filteredSql = new Sql(sql.SQL, sql.Arguments); + var args = filteredSql.Arguments.Concat(filterResult.Item2).ToArray(); + filteredSql = new Sql( + string.Format("{0} {1}", filteredSql.SQL, filterResult.Item1), + args); + filteredSql = new Sql(filteredSql.SQL, args); + } + else + { + //copy to var so that the original isn't changed + filteredSql = new Sql(sql.SQL, sql.Arguments); + } + return filteredSql; + } + + private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy) + { + //copy to var so that the original isn't changed + var sortedSql = new Sql(sql.SQL, sql.Arguments); + + // Apply order according to parameters + if (string.IsNullOrEmpty(orderBy) == false) + { + //each order by param needs to be in a bracket! see: https://github.com/toptensoftware/PetaPoco/issues/177 + var orderByParams = new[] { string.Format("({0})", orderBy) }; + if (orderDirection == Direction.Ascending) + { + sortedSql.OrderBy(orderByParams); + } + else + { + sortedSql.OrderByDescending(orderByParams); + } + } + return sortedSql; + } + internal IEnumerable GetNextUsers(int id, int count) { var idsQuery = new Sql() - .Select("umbracoUser.Id") + .Select("umbracoUser.id") .From(SqlSyntax) .Where(x => x.Id >= id) .OrderBy(x => x.Id, SqlSyntax); diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index d0d2abfe72..ddaf44e6d9 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Services { @@ -9,6 +10,10 @@ namespace Umbraco.Core.Services ///
public interface IUserService : IMembershipUserService { + IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, + UserState? userState = null, string[] userGroups = null, string filter = ""); + /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 8206ab4bca..156e52f9a8 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -2,11 +2,13 @@ using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; +using System.Linq.Expressions; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; @@ -202,7 +204,7 @@ namespace Umbraco.Core.Services } Save(membershipUser); } - + /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// @@ -493,6 +495,55 @@ namespace Umbraco.Core.Services } } + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState? userState = null, string[] userGroups = null, string filter = "") + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + Expression> sort; + switch (orderBy.ToUpperInvariant()) + { + case "USERNAME": + sort = member => member.Username; + break; + case "LANGUAGE": + sort = member => member.Language; + break; + case "NAME": + sort = member => member.Name; + break; + case "EMAIL": + sort = member => member.Email; + break; + case "ID": + sort = member => member.Id; + break; + case "CREATEDATE": + sort = member => member.CreateDate; + break; + case "UPDATEDATE": + sort = member => member.UpdateDate; + break; + case "ISAPPROVED": + sort = member => member.IsApproved; + break; + case "ISLOCKEDOUT": + sort = member => member.IsLockedOut; + break; + default: + throw new IndexOutOfRangeException("The orderBy parameter " + orderBy + " is not valid"); + } + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter) || x.Username.Contains(filter)); + } + + var repository = RepositoryFactory.CreateUserRepository(uow); + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, sort, orderDirection, userGroups, userState, filterQuery); + } + } + /// /// Gets a list of paged objects /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 12d142b7b0..d3bc3e9f82 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -343,6 +343,7 @@ + diff --git a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs index 06a59815b1..e6447d1f45 100644 --- a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs @@ -59,6 +59,7 @@ namespace Umbraco.Web.Cache if (UserGroupPermissionsCache) { + //TODO: Is this good enough for all users attached to this? var keyStartsWith = string.Format("{0}{1}", CacheKeys.UserGroupPermissionsCacheKey, id); UserGroupPermissionsCache.Result.ClearCacheByKeySearch(keyStartsWith); } From 5e3e9cce12b0a194a6353d1a4390508b4d97b6fc Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 17 May 2017 12:10:50 +0200 Subject: [PATCH 064/714] add setting for button sizes + allow icon on buttons --- .../components/buttons/umbbutton.directive.js | 6 +++-- .../less/components/buttons/umb-button.less | 26 +++++++++++++++++++ .../views/components/buttons/umb-button.html | 12 ++++----- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js index 47901a533c..d075bf1118 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js @@ -62,7 +62,8 @@ Use this directive to render an umbraco button. The directive can be used to gen @param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c"). @param {string=} label Set the button label. @param {string=} labelKey Set a localization key to make a multi lingual button ("general_buttonText"). -@param {string=} icon Set a button icon. Can only be used when buttonStyle is "link". +@param {string=} icon Set a button icon. +@param {string=} size Set a button icon ("xs", "m", "l", "xl"). @param {boolean=} disabled Set to true to disable the button. **/ @@ -122,7 +123,8 @@ Use this directive to render an umbraco button. The directive can be used to gen label: "@?", labelKey: "@?", icon: "@?", - disabled: "=" + disabled: "=", + size: "@?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less index d5c71e065a..01bdb2c6ca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less @@ -106,3 +106,29 @@ transform: rotate(360deg); } } + +/* Sizes */ +.umb-button--xs { + padding: 5px 16px; + font-size: 13px; +} + +.umb-button--s { + padding: 6px 16px; + font-size: 15px; +} + +.umb-button--m { + padding: 10px 24px; + font-size: 15px; +} + +.umb-button--l { + padding: 14px 40px; + font-size: 16px; +} + +.umb-button--xl { + padding: 18px 52px; + font-size: 16px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index eb68a0c952..adfc31eb8a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -8,25 +8,25 @@
-
+ - + {{label}} {{label}} - -
/// private Sql GetQueryWithGroups() - { + { //base query includes user groups var sql = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*"); + AddGroupLeftJoin(sql); + return sql; + } + + private void AddGroupLeftJoin(Sql sql) + { sql.LeftJoin(SqlSyntax) .On(SqlSyntax, dto => dto.UserId, dto => dto.Id) .LeftJoin(SqlSyntax) .On(SqlSyntax, dto => dto.Id, dto => dto.UserGroupId) .LeftJoin(SqlSyntax) - .On(SqlSyntax, dto => dto.UserGroupId, dto => dto.Id); - return sql; + .On(SqlSyntax, dto => dto.UserGroupId, dto => dto.Id); } private Sql GetBaseQuery(string columns) @@ -168,6 +173,24 @@ namespace Umbraco.Core.Persistence.Repositories var id = Convert.ToInt32(Database.Insert(userDto)); entity.Id = id; + if (entity.IsPropertyDirty("Groups")) + { + //lookup all assigned + var assigned = entity.Groups == null || entity.Groups.Any() == false + ? new List() + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups }); + + foreach (var groupDto in assigned) + { + var dto = new User2UserGroupDto + { + UserGroupId = groupDto.Id, + UserId = entity.Id + }; + Database.Insert(dto); + } + } + entity.ResetDirtyProperties(); } @@ -182,9 +205,7 @@ namespace Umbraco.Core.Persistence.Repositories } var userDto = userFactory.BuildDto(entity); - - var dirtyEntity = (ICanBeDirty)entity; - + //build list of columns to check for saving - we don't want to save the password if it hasn't changed! //List the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that var colsToSave = new Dictionary() @@ -207,19 +228,19 @@ namespace Umbraco.Core.Persistence.Repositories //create list of properties that have changed var changedCols = colsToSave - .Where(col => dirtyEntity.IsPropertyDirty(col.Value)) + .Where(col => entity.IsPropertyDirty(col.Value)) .Select(col => col.Key) .ToList(); // DO NOT update the password if it has not changed or if it is null or empty - if (dirtyEntity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) + if (entity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) { changedCols.Add("userPassword"); //special case - when using ASP.Net identity the user manager will take care of updating the security stamp, however // when not using ASP.Net identity (i.e. old membership providers), we'll need to take care of updating this manually // so we can just detect if that property is dirty, if it's not we'll set it manually - if (dirtyEntity.IsPropertyDirty("SecurityStamp") == false) + if (entity.IsPropertyDirty("SecurityStamp") == false) { userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); changedCols.Add("securityStampToken"); @@ -231,24 +252,27 @@ namespace Umbraco.Core.Persistence.Repositories { Database.Update(userDto, changedCols); } - - //lookup all assigned - var assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new {aliases = entity.Groups}); - - //first delete all - //TODO: We could do this a nicer way instead of "Nuke and Pave" - Database.Delete("WHERE UserId = @UserId", new { UserId = entity.Id }); - foreach (var groupDto in assigned) - { - var dto = new User2UserGroupDto + if (entity.IsPropertyDirty("Groups")) + { + //lookup all assigned + var assigned = entity.Groups == null || entity.Groups.Any() == false + ? new List() + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups }); + + //first delete all + //TODO: We could do this a nicer way instead of "Nuke and Pave" + Database.Delete("WHERE UserId = @UserId", new { UserId = entity.Id }); + + foreach (var groupDto in assigned) { - UserGroupId = groupDto.Id, - UserId = entity.Id - }; - Database.Insert(dto); + var dto = new User2UserGroupDto + { + UserGroupId = groupDto.Id, + UserId = entity.Id + }; + Database.Insert(dto); + } } entity.ResetDirtyProperties(); @@ -372,9 +396,12 @@ namespace Umbraco.Core.Persistence.Repositories private IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, string[] userGroups = null, UserState? userState = null, IQuery filter = null) { if (string.IsNullOrWhiteSpace(orderBy)) throw new ArgumentException("Value cannot be null or whitespace.", "orderBy"); - - var filterSql = new Sql(); + + Sql filterSql = null; + if (filter != null || (userGroups != null && userGroups.Length > 0)) + filterSql = new Sql(); + if (filter != null) { foreach (var filterClaus in filter.GetWhereClauses()) @@ -382,100 +409,75 @@ namespace Umbraco.Core.Persistence.Repositories filterSql.Append(string.Format("AND ({0})", filterClaus.Item1), filterClaus.Item2); } } - Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); - + if (userGroups != null && userGroups.Length > 0) + { + var subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id + FROM umbracoUser + INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id + INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId + WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; + filterSql.Append(subQuery, new {userGroups= userGroups}); + } + // Get base query for returning IDs var sqlBaseIds = GetBaseQuery("id"); - // Get base query for returning all data - var sqlBaseFull = GetBaseQuery(false); - + if (query == null) query = new Query(); var translatorIds = new SqlTranslator(sqlBaseIds, query); - var sqlQueryIds = translatorIds.Translate(); - var translatorFull = new SqlTranslator(sqlBaseFull, query); - var sqlQueryFull = translatorFull.Translate(); - + var sqlQueryIds = translatorIds.Translate(); + //get sorted and filtered sql var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(sqlQueryIds, filterCallback), + GetFilteredSqlForPagedResults(sqlQueryIds, filterSql), orderDirection, orderBy); // Get page of results and total count - IEnumerable result; var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); - - return Enumerable.Empty(); - - ////NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number - //// that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in - //// the pageResult, then the GetAll will actually return ALL records in the db. - //if (pagedResult.Items.Any()) - //{ - // //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query - // var args = sqlNodeIdsWithSort.Arguments; - // string sqlStringCount, sqlStringPage; - // Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - - // //We need to make this FULL query an inner join on the paged ID query - // var splitQuery = sqlQueryFull.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); - // var fullQueryWithPagedInnerJoin = new Sql(splitQuery[0]) - // .Append("INNER JOIN (") - // //join the paged query with the paged query arguments - // .Append(sqlStringPage, args) - // .Append(") temp ") - // .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) - // //add the original where clause back with the original arguments - // .Where(splitQuery[1], sqlQueryIds.Arguments); - - // //get sorted and filtered sql - // var fullQuery = GetSortedSqlForPagedResults( - // GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, defaultFilter), - // orderDirection, orderBy, orderBySystemField, nodeIdSelect); - - // return processQuery(fullQuery, new PagingSqlQuery(Database, sqlNodeIdsWithSort, pageIndex, pageSize)); - //} - //else - //{ - // result = Enumerable.Empty(); - //} - - //return result; + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + //NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number + // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in + // the pageResult. + if (pagedResult.Items.Any()) + { + //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + var args = sqlNodeIdsWithSort.Arguments; + string sqlStringCount, sqlStringPage; + Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - //return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - // new Tuple("cmsMember", "nodeId"), - // (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, - // filterCallback); + var sqlQueryFull = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*"); + + var fullQueryWithPagedInnerJoin = sqlQueryFull + .Append("INNER JOIN (") + //join the paged query with the paged query arguments + .Append(sqlStringPage, args) + .Append(") temp ") + .Append("ON umbracoUser.id = temp.id"); + AddGroupLeftJoin(fullQueryWithPagedInnerJoin); - //var sql = new Sql() - // .Select("umbracoUser.Id") - // .From(SqlSyntax); + //get sorted and filtered sql + var fullQuery = GetSortedSqlForPagedResults( + GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, filterSql), + orderDirection, orderBy); - //var idsQuery = query == null ? sql : new SqlTranslator(sql, query).Translate(); + var users = ConvertFromDtos(Database.Fetch(new UserGroupRelator().Map, fullQuery)) + .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. - //// need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177 - //idsQuery.OrderBy("(" + orderBy + ")"); - //var page = Database.Page(pageIndex + 1, pageSize, idsQuery); - //totalRecords = Convert.ToInt32(page.TotalItems); + return users; + } - //if (totalRecords == 0) - // return Enumerable.Empty(); - - //// now get the actual users and ensure they are ordered properly (same clause) - //var ids = page.Items.ToArray(); - //return ids.Length == 0 ? Enumerable.Empty() : GetAll(ids).OrderBy(orderBy.Compile()); + return Enumerable.Empty(); } - private Sql GetFilteredSqlForPagedResults(Sql sql, Func> defaultFilter = null) + private Sql GetFilteredSqlForPagedResults(Sql sql, Sql filterSql) { Sql filteredSql; // Apply filter - if (defaultFilter != null) + if (filterSql != null) { - var filterResult = defaultFilter(); + var sqlFilter = " WHERE " + filterSql.SQL.TrimStart("AND "); //NOTE: this is certainly strange - NPoco handles this much better but we need to re-create the sql // instance a couple of times to get the parameter order correct, for some reason the first @@ -483,9 +485,9 @@ namespace Umbraco.Core.Persistence.Repositories // accordingly - so we re-create it again. In v8 we don't need to do this and it's already taken care of. filteredSql = new Sql(sql.SQL, sql.Arguments); - var args = filteredSql.Arguments.Concat(filterResult.Item2).ToArray(); + var args = filteredSql.Arguments.Concat(filterSql.Arguments).ToArray(); filteredSql = new Sql( - string.Format("{0} {1}", filteredSql.SQL, filterResult.Item1), + string.Format("{0} {1}", filteredSql.SQL, sqlFilter), args); filteredSql = new Sql(filteredSql.SQL, args); } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index d3bc3e9f82..01329ab6eb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -505,6 +505,7 @@ + diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index fc2b870eef..0ad8fe1708 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -58,6 +58,38 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Add_With_Group() + { + var group = MockedUserGroup.CreateUserGroup(); + + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateUserGroupRepository(unitOfWork)) + { + repository.AddOrUpdate(group); + unitOfWork.Commit(); + } + + using (var repository = CreateRepository(unitOfWork)) + { + IUser user = MockedUser.CreateUser(); + user.AddGroup(group.Alias); + + // Act + repository.AddOrUpdate(user); + unitOfWork.Commit(); + + user = repository.Get(user.Id); + + // Assert + Assert.That(user.HasIdentity, Is.True); + Assert.AreEqual(1, user.Groups.Count()); + Assert.AreEqual(group.Alias, user.Groups.ElementAt(0)); + } + } + [Test] public void Can_Perform_Multiple_Adds_On_UserRepository() { diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 02a4f335cc..682b3695d7 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using umbraco.BusinessLogic.Actions; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Services { @@ -309,6 +310,72 @@ namespace Umbraco.Tests.Services Assert.AreEqual("test0", found.Last().Username); } + [Test] + public void Get_All_Paged_Users_With_Filter() + { + var users = MockedUser.CreateMulipleUsers(10).ToArray(); + ServiceContext.UserService.Save(users); + + long totalRecs; + var found = ServiceContext.UserService.GetAll(0, 2, out totalRecs, "username", Direction.Ascending, filter: "test"); + + Assert.AreEqual(2, found.Count()); + Assert.AreEqual(10, totalRecs); + Assert.AreEqual("test0", found.First().Username); + Assert.AreEqual("test1", found.Last().Username); + } + + [Test] + public void Get_All_Paged_Users_For_Group() + { + var userGroup = MockedUserGroup.CreateUserGroup(); + ServiceContext.UserService.Save(userGroup); + + var users = MockedUser.CreateMulipleUsers(10).ToArray(); + for (var i = 0; i < 10;) + { + users[i].AddGroup(userGroup.Alias); + i = i + 2; + } + ServiceContext.UserService.Save(users); + + long totalRecs; + var found = ServiceContext.UserService.GetAll(0, 2, out totalRecs, "username", Direction.Ascending, userGroups: new[] {userGroup.Alias}); + + Assert.AreEqual(2, found.Count()); + Assert.AreEqual(5, totalRecs); + Assert.AreEqual("test0", found.First().Username); + Assert.AreEqual("test2", found.Last().Username); + } + + [Test] + public void Get_All_Paged_Users_For_Group_With_Filter() + { + var userGroup = MockedUserGroup.CreateUserGroup(); + ServiceContext.UserService.Save(userGroup); + + var users = MockedUser.CreateMulipleUsers(10).ToArray(); + for (var i = 0; i < 10;) + { + users[i].AddGroup(userGroup.Alias); + i = i + 2; + } + for (var i = 0; i < 10;) + { + users[i].Name = "blah" + users[i].Name; + i = i + 3; + } + ServiceContext.UserService.Save(users); + + long totalRecs; + var found = ServiceContext.UserService.GetAll(0, 2, out totalRecs, "username", Direction.Ascending, userGroups: new[] { userGroup.Alias }, filter: "blah"); + + Assert.AreEqual(2, found.Count()); + Assert.AreEqual(2, totalRecs); + Assert.AreEqual("test0", found.First().Username); + Assert.AreEqual("test6", found.Last().Username); + } + [Test] public void Count_All_Users() { From 4c61cac9a8bd2863618c634f73d82e6672ab9752 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 May 2017 22:58:32 +1000 Subject: [PATCH 068/714] rename to users controller --- .../Editors/BackOfficeController.cs | 2 +- .../{UserController.cs => UsersController.cs} | 45 +++++-------------- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 3 files changed, 12 insertions(+), 37 deletions(-) rename src/Umbraco.Web/Editors/{UserController.cs => UsersController.cs} (64%) diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index c49f5b973a..bb37b22235 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -237,7 +237,7 @@ namespace Umbraco.Web.Editors controller => controller.GetEmbed("", 0, 0)) }, { - "userApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + "userApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.PostDisableUser(0)) }, { diff --git a/src/Umbraco.Web/Editors/UserController.cs b/src/Umbraco.Web/Editors/UsersController.cs similarity index 64% rename from src/Umbraco.Web/Editors/UserController.cs rename to src/Umbraco.Web/Editors/UsersController.cs index 7f24f93d4c..a93bb0e251 100644 --- a/src/Umbraco.Web/Editors/UserController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -19,12 +19,12 @@ namespace Umbraco.Web.Editors { [PluginController("UmbracoApi")] [UmbracoApplicationAuthorize(Constants.Applications.Users)] - public class UserController : UmbracoAuthorizedJsonController + public class UsersController : UmbracoAuthorizedJsonController { /// /// Constructor /// - public UserController() + public UsersController() : this(UmbracoContext.Current) { } @@ -33,7 +33,7 @@ namespace Umbraco.Web.Editors /// Constructor ///
/// - public UserController(UmbracoContext umbracoContext) + public UsersController(UmbracoContext umbracoContext) : base(umbracoContext) { } @@ -53,47 +53,22 @@ namespace Umbraco.Web.Editors return Mapper.Map(user); } - //TODO: This will probably not be UserDisplay objects since there's probably too much data in the display object for a grid public PagedResult GetPagedUsers( int id, int pageNumber = 1, int pageSize = 0, - string orderBy = "SortOrder", + string orderBy = "username", Direction orderDirection = Direction.Ascending, + string[] userGroups = null, string filter = "") { + long pageIndex = pageNumber - 1; + long total; + var result = Services.UserService.GetAll(pageIndex, pageSize, out total, orderBy, orderDirection, null, userGroups, filter); - //TODO: Make this real, for now this is mock data - - var startId = 100 + ((pageNumber -1) * pageSize); - var numUsers = pageSize; - var users = new List(); - var userTypes = Services.UserService.GetAllUserGroups().ToDictionary(x => x.Alias, x => x.Name); - var cultures = Services.TextService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); - for (int i = 0; i < numUsers; i++) + return new PagedResult(total, pageNumber, pageSize) { - var display = new UserDisplay - { - Id = startId, - //UserType = "writer", - AllowedSections = new[] {"content", "media"}, - AvailableUserGroups = userTypes, - Email = "test" + startId + "@test.com", - Name = "User " + startId, - Culture = "en-US", - AvailableCultures = cultures, - Path = "-1," + startId, - ParentId = -1, - StartContentId = -1, - StartMediaId = -1 - }; - users.Add(display); - startId++; - } - - return new PagedResult(100, pageNumber, pageSize) - { - Items = users + Items = Mapper.Map>(result) }; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9a2ed9a0c0..43b85365d7 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -521,7 +521,7 @@ - + From b130ccf9c14fd26a466c2c1996b7c22244ea3e46 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 18 May 2017 15:53:54 +1000 Subject: [PATCH 069/714] removes userResource and adds the disableUser to the usersResource --- .../lib/umbraco/LegacyUmbClientMgr.js | 2 +- .../src/common/resources/user.resource.js | 25 ------------------- .../src/common/resources/users.resource.js | 16 +++++++++++- 3 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/user.resource.js diff --git a/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js b/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js index 06d6c11c71..bc5980c7cb 100644 --- a/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js +++ b/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js @@ -194,7 +194,7 @@ Umbraco.Sys.registerNamespace("Umbraco.Application"); if (currentMenuNode) { if (confirm(txtConfirmDisable + ' "' + UmbClientMgr.mainTree().getActionNode().nodeName + '"?\n\n')) { angularHelper.safeApply($rootScope, function () { - userResource.disableUser(currentMenuNode.nodeId).then(function () { + usersResource.disableUser(currentMenuNode.nodeId).then(function () { UmbClientMgr.mainTree().syncTree("-1," + currentMenuNode.nodeId, true); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/user.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/user.resource.js deleted file mode 100644 index 0c6f851eb0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/resources/user.resource.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @ngdoc service - * @name umbraco.resources.userResource - **/ -function userResource($q, $http, umbDataFormatter, umbRequestHelper) { - - return { - - disableUser: function (userId) { - - if (!userId) { - throw "userId not specified"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostDisableUser", [{ userId: userId }])), - 'Failed to disable the user ' + userId); - } - }; -} - -angular.module('umbraco.resources').factory('userResource', userResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index dbb07efca9..c6e4439d7d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -11,6 +11,19 @@ function usersResource($http, umbRequestHelper, $q) { + function disableUser(userId) { + if (!userId) { + throw "userId not specified"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostDisableUser", [{ userId: userId }])), + 'Failed to disable the user ' + userId); + } + function getUser() { var deferred = $q.defer(); var user = { @@ -288,7 +301,8 @@ getUser: getUser, getUsers: getUsers, getUserRole: getUserRole, - getUserGroups: getUserGroups + getUserGroups: getUserGroups, + disableUser: disableUser }; return resource; From 29177974eb04473006a80e10bbaf6de53664887b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 18 May 2017 16:38:14 +1000 Subject: [PATCH 070/714] adds the getPagedResults method to the usersResource --- .../src/common/resources/users.resource.js | 74 +++++++++++++++---- .../services/umbrequesthelper.service.js | 2 +- src/Umbraco.Web/Editors/UsersController.cs | 1 - 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index c6e4439d7d..4c1f7fa1a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -12,16 +12,63 @@ function usersResource($http, umbRequestHelper, $q) { function disableUser(userId) { - if (!userId) { - throw "userId not specified"; + if (!userId) { + throw "userId not specified"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostDisableUser", [{ userId: userId }])), + 'Failed to disable the user ' + userId); + } + + function getPagedResults(options) { + var defaults = { + pageSize: 25, + pageNumber: 1, + filter: '', + orderDirection: "Ascending", + orderBy: "Username", + userGroups: [] + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + var params = { + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + filter: options.filter + }; + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = umbRequestHelper.dictionaryToQueryString(params); + if (options.userGroups.length > 0) { + //we need to create a custom query string for an array + qry += "&" + options.userGroups.join("&"); } return umbRequestHelper.resourcePromise( - $http.post( + $http.get( umbRequestHelper.getApiUrl( "userApiBaseUrl", - "PostDisableUser", [{ userId: userId }])), - 'Failed to disable the user ' + userId); + "GetPagedUsers", + qry)), + 'Failed to retrieve users paged result'); } function getUser() { @@ -270,7 +317,7 @@ "alias": "writer", "id": 2, "icon": "icon-edit", - "sections": [{ "id": "1", "name": "Content"}, { "id": "2", "name": "Media"}], + "sections": [{ "id": "1", "name": "Content" }, { "id": "2", "name": "Media" }], "startNodesContent": [], "startNodesMedia": [] }, @@ -279,18 +326,18 @@ "alias": "editor", "id": 3, "icon": "icon-tools", - "sections": [{ "id": "1", "name": "Content"}, { "id": "2", "name": "Media"}], - "startNodesContent": [{ "id": "1", "name": "Start node 1"}, { "id": "2", "name": "Start node 2"}], - "startNodesMedia": [{ "id": "1", "name": "Start node 1"}, { "id": "2", "name": "Start node 2"}] + "sections": [{ "id": "1", "name": "Content" }, { "id": "2", "name": "Media" }], + "startNodesContent": [{ "id": "1", "name": "Start node 1" }, { "id": "2", "name": "Start node 2" }], + "startNodesMedia": [{ "id": "1", "name": "Start node 1" }, { "id": "2", "name": "Start node 2" }] }, { "name": "Translator", "alias": "translator", "id": 4, "icon": "icon-globe", - "sections": [{ "id": "1", "name": "Content"}, { "id": "2", "name": "Translations"}], - "startNodesContent": [{ "id": "1", "name": "Start node 1"}], - "startNodesMedia": [{ "id": "1", "name": "Start node 1"}] + "sections": [{ "id": "1", "name": "Content" }, { "id": "2", "name": "Translations" }], + "startNodesContent": [{ "id": "1", "name": "Start node 1" }], + "startNodesMedia": [{ "id": "1", "name": "Start node 1" }] } ]; deferred.resolve(userGroups); @@ -302,7 +349,8 @@ getUsers: getUsers, getUserRole: getUserRole, getUserGroups: getUserGroups, - disableUser: disableUser + disableUser: disableUser, + getPagedResults: getPagedResults }; return resource; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 67df26d50a..c31eac7697 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -37,7 +37,7 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ * @function * * @description - * This will turn an array of key/value pairs into a query string + * This will turn an array of key/value pairs or a standard dictionary into a query string * * @param {Array} queryStrings An array of key/value pairs */ diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index a93bb0e251..0d48b96bd0 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -54,7 +54,6 @@ namespace Umbraco.Web.Editors } public PagedResult GetPagedUsers( - int id, int pageNumber = 1, int pageSize = 0, string orderBy = "username", From d90ef2865b63ae1ba7fd6b0fca469d149f2a76b2 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 18 May 2017 11:33:37 +0200 Subject: [PATCH 071/714] don't use bold for badges --- src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less index a63e799dc7..6998afa556 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less @@ -1,7 +1,6 @@ .umb-badge { padding: 6px 8px; margin: 0 5px 0 0; - font-weight: 600; color: @black; background-color: @turquoise-washed; border-width: 1px; From d57a273c65798439dfef010826b0f51103e9f198 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 18 May 2017 13:33:57 +0200 Subject: [PATCH 072/714] Add support for multiple styles on a button component --- .../components/buttons/umbbutton.directive.js | 21 +++++++++++++++++-- .../less/components/buttons/umb-button.less | 1 + 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js index d075bf1118..cb0e4c92fb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js @@ -57,7 +57,7 @@ Use this directive to render an umbraco button. The directive can be used to gen @param {callback} action The button action which should be performed when the button is clicked. @param {string=} href Url/Path to navigato to. @param {string=} type Set the button type ("button" or "submit"). -@param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link"). +@param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link", "block"). Pass in array to add multple styles [success,block]. @param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). @param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c"). @param {string=} label Set the button label. @@ -83,7 +83,24 @@ Use this directive to render an umbraco button. The directive can be used to gen } if (scope.buttonStyle) { - scope.style = "btn-" + scope.buttonStyle; + + // make it possible to pass in multiple styles + if(scope.buttonStyle.startsWith("[") && scope.buttonStyle.endsWith("]")) { + + // when using an attr it will always be a string so we need to remove square brackets + // and turn it into and array + var withoutBrackets = scope.buttonStyle.replace(/[\[\]']+/g,''); + // split array by , + make sure to catch whitespaces + var array = withoutBrackets.split(/\s?,\s?/g); + + angular.forEach(array, function(item){ + scope.style = scope.style + " " + "btn-" + item; + }); + + } else { + scope.style = "btn-" + scope.buttonStyle; + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less index 01bdb2c6ca..98a3b7bb82 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less @@ -19,6 +19,7 @@ display: flex; flex-wrap: wrap; align-items: center; + justify-content: center; } .umb-button__icon { From fbed82219d7788c102252458e617c3aad61eacc9 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 18 May 2017 15:23:38 +0200 Subject: [PATCH 073/714] init edit user editor --- .../src/common/resources/users.resource.js | 5 +- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../src/less/components/umb-avatar.less | 8 +- .../users/umb-user-role-preview.less | 2 +- .../src/less/components/users/umb-user.less | 20 ++ .../src/views/users/user.controller.js | 104 +++++++++ .../src/views/users/user.html | 214 ++++++++++++++++++ 7 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/users/umb-user.less diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index dd307553e7..b69098a9ae 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -21,7 +21,10 @@ "lastLogin": "2014-04-25T01:32:21.196Z", "invitedBy": "Edward Flores", "createdBy": "", - "state": "active" + "state": "active", + "userRoles": [], + "startNodesContent": [], + "startNodesMedia": [] }; deferred.resolve(user); return deferred.promise; diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 887b9e7816..637b13b965 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -135,6 +135,7 @@ @import "components/users/umb-users.less"; @import "components/users/umb-user-role-picker-list.less"; @import "components/users/umb-user-role-preview.less"; +@import "components/users/umb-user.less"; // Utilities @import "utilities/_flexbox.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less index 61441f16b0..85b597ab91 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less @@ -50,6 +50,12 @@ font-size: 20px; } +.umb-avatar--xxl { + width: 150px; + height: 150px; + font-size: 20px; +} + /* Colors */ .umb-avatar--white { @@ -68,7 +74,7 @@ } .umb-avatar--secondary { - background-color: @purple-l1; + background-color: @purple-l3; color: @white; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-preview.less index a772a8ff28..a5c9d6ace1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-role-preview.less @@ -3,7 +3,7 @@ padding-bottom: 10px; display: flex; box-sizing: border-box; - border-bottom: 1px solid @gray-9; + border-bottom: 1px solid @gray-8; } .umb-user-role-preview:last-of-type { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user.less new file mode 100644 index 0000000000..c4d696c6f8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user.less @@ -0,0 +1,20 @@ +.test-group { + border: 1px solid @gray-8; + border-radius: 3px; + margin-bottom: 30px; +} + +.test-group-title { + padding: 10px 20px; + margin-bottom: 0; + background-color: @gray-10; + color: @black; + border-bottom: none; + font-size: 16px; + font-weight: bold; + color: @black; +} + +.test-group-content { + padding: 20px; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index fd3950bde6..1d8ba3ff84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -10,6 +10,14 @@ vm.user = {}; vm.goBack = goBack; + vm.openUserRolePicker = openUserRolePicker; + vm.openContentPicker = openContentPicker; + vm.openMediaPicker = openMediaPicker; + vm.removeSelectedItem = removeSelectedItem; + vm.disableUser = disableUser; + vm.resetPassword = resetPassword; + vm.getUserStateType = getUserStateType; + vm.changeAvatar = changeAvatar; function init() { @@ -30,6 +38,102 @@ function goBack() { $location.path("/users/users/overview").search("subview", "users"); } + + function openUserRolePicker() { + vm.userRolePicker = { + title: "Select user roles", + view: "userrolepicker", + selection: vm.user.userRoles, + closeButtonLabel: "Cancel", + show: true, + submit: function(model) { + // apply changes + if(model.selection) { + vm.user.userRoles = model.selection; + } + vm.userRolePicker.show = false; + vm.userRolePicker = null; + }, + close: function(oldModel) { + // rollback on close + if(oldModel.selection) { + vm.user.userRoles = oldModel.selection; + } + vm.userRolePicker.show = false; + vm.userRolePicker = null; + } + }; + } + + function openContentPicker() { + vm.contentPicker = { + title: "Select content start node", + view: "contentpicker", + multiPicker: true, + show: true, + submit: function(model) { + if(model.selection) { + vm.user.startNodesContent = model.selection; + } + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function(oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + + function openMediaPicker() { + vm.contentPicker = { + title: "Select media start node", + view: "treepicker", + section: "media", + treeAlias: "media", + entityType: "media", + multiPicker: true, + show: true, + submit: function(model) { + if(model.selection) { + vm.user.startNodesMedia = model.selection; + } + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function(oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + + function removeSelectedItem(index, selection) { + selection.splice(index, 1); + } + + function disableUser() { + alert("disable user"); + } + + function resetPassword() { + alert("reset password"); + } + + function getUserStateType(state) { + switch (state) { + case "disabled" || "umbracoDisabled": + return "danger"; + case "pending": + return "warning"; + default: + return "success"; + } + } + + function changeAvatar() { + alert("change avatar"); + } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 208a941200..22d9788148 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -21,6 +21,199 @@ +
+ +
+ +
+ +
+
Meta
+
+ + + + + + + +
+
+ +
+
Permissions
+
+ + + + + + + + Add + + + + + + + + + + + Add + + + + + + + + + + + Add + + + + +
+
+
+ +
+ +
+ +
+ + + + +
+ + + +
+
Last login:
+
+ {{ vm.user.lastLogin }} + The user has not logged in yet +
+
+ +
+
Invited by:
+
{{ vm.user.invitedBy }}
+
+ +
+
Created by:
+
{{ vm.user.createdBy }}
+
+ +
+ +
+ +
+ + +
+ +
+ + +
+ + + + +
+ +
+ +
+ +
+ @@ -45,4 +238,25 @@ + + + + + + + + +
From aeabca87d70acd5f0ed1c2785b7a440a18513234 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 18 May 2017 15:25:05 +0200 Subject: [PATCH 074/714] init edit role editor --- .../users/umbuserpreview.directive.js | 29 ++++ .../src/common/resources/users.resource.js | 28 +++- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../components/users/umb-user-preview.less | 52 +++++++ .../components/users/umb-user-preview.html | 20 +++ .../src/views/users/role.controller.js | 67 +++++++++ .../src/views/users/role.html | 136 +++++++++++++++++- 7 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbuserpreview.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-preview.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbuserpreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbuserpreview.directive.js new file mode 100644 index 0000000000..29db5f7238 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbuserpreview.directive.js @@ -0,0 +1,29 @@ +(function () { + 'use strict'; + + function UserPreviewDirective() { + + function link(scope, el, attr, ctrl) { + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/users/umb-user-preview.html', + scope: { + avatar: "=?", + name: "=", + allowRemove: "=?", + onRemove: "&?" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbUserPreview', UserPreviewDirective); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index b69098a9ae..77cc8b2978 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -243,7 +243,33 @@ "name": "Admin", "alias": "admin", "id": 1, - "icon": "icon-medal" + "icon": "icon-medal", + "users": [ + { + "id": "1", + "name": "Angela Stone", + "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/jina/128.jpg", + "state": "active" + }, + { + "id": "1", + "name": "Beverly Silva", + "avatar": "", + "state": "disabled" + }, + { + "id": "1", + "name": "Ruth Turner", + "avatar": "", + "state": "pending" + }, + { + "id": "1", + "name": "Arthur Welch", + "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/ashleyford/128.jpg", + "state": "active" + } + ] }; deferred.resolve(user); return deferred.promise; diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 637b13b965..338b22bf52 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -135,6 +135,7 @@ @import "components/users/umb-users.less"; @import "components/users/umb-user-role-picker-list.less"; @import "components/users/umb-user-role-preview.less"; +@import "components/users/umb-user-preview.less"; @import "components/users/umb-user.less"; // Utilities diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-preview.less new file mode 100644 index 0000000000..ac60786eb3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-preview.less @@ -0,0 +1,52 @@ +.umb-user-preview { + padding-top: 10px; + padding-bottom: 10px; + display: flex; + box-sizing: border-box; + border-bottom: 1px solid @gray-8; +} + +.umb-user-preview:last-of-type { + border-bottom: none; + margin-bottom: 5px; +} + +.umb-user-preview__avatar { + margin-right: 15px; +} + +.umb-user-preview__content { + flex: 1 1 auto; +} + +.umb-user-preview__name { + font-size: 15px; + font-weight: bold; + color: @black; + margin-bottom: 3px; + margin-top: 2px; +} + +.umb-user-preview__actions { + flex: 0 0 auto; + display: flex; + align-items: center; +} + +.umb-user-preview__action { + margin-left: 5px; + margin-right: 5px; + font-size: 13px; + font-weight: bold; + color: @gray-5; +} + +.umb-user-preview__action:hover { + color: @turquoise; + text-decoration: none; + opacity: 1; +} + +.umb-user-preview__action--red:hover { + color: @red; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html new file mode 100644 index 0000000000..7753376206 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html @@ -0,0 +1,20 @@ +
+ +
+ + +
+ +
+
{{ name }}
+
+ +
+ Remove +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js index 12cb81e08d..b44103e726 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js @@ -10,6 +10,11 @@ vm.userRole = {}; vm.goBack = goBack; + vm.openSectionPicker = openSectionPicker; + vm.openContentPicker = openContentPicker; + vm.openMediaPicker = openMediaPicker; + vm.removeSelectedItem = removeSelectedItem; + vm.getUserStateType = getUserStateType; function init() { @@ -30,6 +35,68 @@ function goBack() { $location.path("/users/users/overview").search("subview", "roles"); } + + function openSectionPicker() { + alert("open section picker"); + } + + function openContentPicker() { + vm.contentPicker = { + title: "Select content start node", + view: "contentpicker", + multiPicker: true, + show: true, + submit: function(model) { + if(model.selection) { + vm.userRole.startNodesContent = model.selection; + } + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function(oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + + function openMediaPicker() { + vm.contentPicker = { + title: "Select media start node", + view: "treepicker", + section: "media", + treeAlias: "media", + entityType: "media", + multiPicker: true, + show: true, + submit: function(model) { + if(model.selection) { + vm.userRole.startNodesMedia = model.selection; + } + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function(oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + + function removeSelectedItem(index, selection) { + selection.splice(index, 1); + } + + function getUserStateType(state) { + switch (state) { + case "disabled" || "umbracoDisabled": + return "danger"; + case "pending": + return "warning"; + default: + return "success"; + } + } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/role.html b/src/Umbraco.Web.UI.Client/src/views/users/role.html index e51eda26b1..54cc1abebb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/role.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/role.html @@ -8,7 +8,7 @@ @@ -21,6 +21,119 @@ +
+ +
+ +
+ +
+
Something
+
+ + + + + + + Add + + + + + + + + + Add + + + + + + + + + Add + + + +
+
+ +
+
Default permissions
+
Some content here
+
+ +
+
Granular permissions
+
Some content here
+
+ +
+ +
+ +
+ +
Users
+ + + + + + Add + + +
+ +
+ +
+ +
+ + @@ -45,4 +158,25 @@ + + + + + + + + +
\ No newline at end of file From bb431c7681a6b4b8b48a4038d6dcc79c8d834cee Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 May 2017 09:57:53 +1000 Subject: [PATCH 075/714] updates ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bd7a0ffa9d..06cf562b28 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,5 @@ build/ui-docs.zip build/csharp-docs.zip build/msbuild.log .vs/ -src/packages/ \ No newline at end of file +src/packages/ +src/PrecompiledWeb/* From 048112dceaaf04bdf6dc1c349656ed8c3e59b76d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 May 2017 20:17:50 +1000 Subject: [PATCH 076/714] Adds framework for testing our controllers (borrowed from the rest api project) adds a couple easy ones for the user controller --- src/Umbraco.Tests/App.config | 4 +- .../Controllers/UsersControllerTests.cs | 70 +++++++++ .../AuthenticateEverythingExtensions.cs | 18 +++ .../AuthenticateEverythingMiddleware.cs | 57 +++++++ .../SpecificAssemblyResolver.cs | 21 +++ .../TestControllerActivator.cs | 22 +++ .../TestControllerActivatorBase.cs | 140 ++++++++++++++++++ .../ControllerTesting/TestRunner.cs | 76 ++++++++++ .../ControllerTesting/TestStartup.cs | 54 +++++++ .../ControllerTesting/TraceExceptionLogger.cs | 16 ++ src/Umbraco.Tests/Umbraco.Tests.csproj | 39 ++++- src/Umbraco.Tests/packages.config | 13 +- src/Umbraco.Web/Editors/UsersController.cs | 11 +- src/Umbraco.Web/Security/WebSecurity.cs | 2 +- src/Umbraco.Web/UmbracoContext.cs | 2 +- .../WebApi/AngularJsonMediaTypeFormatter.cs | 17 ++- .../UmbracoApplicationAuthorizeAttribute.cs | 6 +- 17 files changed, 544 insertions(+), 24 deletions(-) create mode 100644 src/Umbraco.Tests/Controllers/UsersControllerTests.cs create mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingExtensions.cs create mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs create mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/SpecificAssemblyResolver.cs create mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivator.cs create mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs create mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs create mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/TestStartup.cs create mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/TraceExceptionLogger.cs diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 6500cd3741..ef5d8133f6 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -156,7 +156,7 @@ - + @@ -172,7 +172,7 @@ - + diff --git a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Controllers/UsersControllerTests.cs new file mode 100644 index 0000000000..45a4979377 --- /dev/null +++ b/src/Umbraco.Tests/Controllers/UsersControllerTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using System.Text; +using System.Web.Configuration; +using System.Web.Http; +using System.Web.Http.SelfHost; +using AutoMapper; +using Examine.Providers; +using Microsoft.Owin.Testing; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.ControllerTesting; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Web.Editors; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; +using Umbraco.Web.WebApi; + +namespace Umbraco.Tests.Controllers +{ + [DatabaseTestBehavior(DatabaseBehavior.NoDatabasePerFixture)] + [RequiresAutoMapperMappings] + [TestFixture] + public class UsersControllerTests : BaseDatabaseFactoryTest + { + [Test] + public async void GetPagedUsers_Empty() + { + var runner = new TestRunner((message, helper) => new UsersController(helper.UmbracoContext)); + var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); + + var obj = JsonConvert.DeserializeObject>(response.Item2); + Assert.AreEqual(0, obj.TotalItems); + } + + [Test] + public async void GetPagedUsers_10() + { + var runner = new TestRunner((message, helper) => + { + //setup some mocks + var userServiceMock = Mock.Get(helper.UmbracoContext.Application.Services.UserService); + var users = MockedUser.CreateMulipleUsers(10); + long outVal = 10; + userServiceMock.Setup(service => service.GetAll(It.IsAny(), It.IsAny(), out outVal, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(() => users); + + return new UsersController(helper.UmbracoContext); + }); + var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); + + var obj = JsonConvert.DeserializeObject>(response.Item2); + Assert.AreEqual(10, obj.TotalItems); + Assert.AreEqual(10, obj.Items.Count()); + } + } +} diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingExtensions.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingExtensions.cs new file mode 100644 index 0000000000..f7d457f0d1 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingExtensions.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Owin.Extensions; +using Owin; + +namespace Umbraco.Tests.TestHelpers.ControllerTesting +{ + public static class AuthenticateEverythingExtensions + { + public static IAppBuilder AuthenticateEverything(this IAppBuilder app) + { + if (app == null) + throw new ArgumentNullException("app"); + app.Use(typeof(AuthenticateEverythingMiddleware), (object)app, (object)new AuthenticateEverythingMiddleware.AuthenticateEverythingAuthenticationOptions()); + app.UseStageMarker(PipelineStage.Authenticate); + return app; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs new file mode 100644 index 0000000000..2dbfa757f2 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Infrastructure; +using Owin; +using Umbraco.Core.Security; + +namespace Umbraco.Tests.TestHelpers.ControllerTesting +{ + public class AuthenticateEverythingMiddleware : AuthenticationMiddleware + { + public AuthenticateEverythingMiddleware(OwinMiddleware next, IAppBuilder app, AuthenticationOptions options) + : base(next, options) + { + } + + protected override AuthenticationHandler CreateHandler() + { + return new AuthenticateEverythingHandler(); + } + + public class AuthenticateEverythingHandler : AuthenticationHandler + { + protected override Task AuthenticateCoreAsync() + { + var identity = new UmbracoBackOfficeIdentity( + new UserData(Guid.NewGuid().ToString()) + { + Id = 0, + Roles = new[] { "admin" }, + AllowedApplications = new[] { "content", "media", "members" }, + Culture = "en-US", + RealName = "Admin", + StartContentNode = -1, + StartMediaNode = -1, + Username = "admin" + }); + + return Task.FromResult(new AuthenticationTicket(identity, + new AuthenticationProperties() + { + ExpiresUtc = DateTime.Now.AddDays(1) + })); + } + } + + public class AuthenticateEverythingAuthenticationOptions : AuthenticationOptions + { + public AuthenticateEverythingAuthenticationOptions() + : base("AuthenticateEverything") + { + AuthenticationMode = AuthenticationMode.Active; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/SpecificAssemblyResolver.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/SpecificAssemblyResolver.cs new file mode 100644 index 0000000000..b301952c92 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/SpecificAssemblyResolver.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Web.Http.Dispatcher; + +namespace Umbraco.Tests.TestHelpers.ControllerTesting +{ + public class SpecificAssemblyResolver : IAssembliesResolver + { + private readonly Assembly[] _assemblies; + + public SpecificAssemblyResolver(Assembly[] assemblies) + { + _assemblies = assemblies; + } + + public ICollection GetAssemblies() + { + return _assemblies; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivator.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivator.cs new file mode 100644 index 0000000000..81cdeccbfe --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivator.cs @@ -0,0 +1,22 @@ +using System; +using System.Net.Http; +using System.Web.Http; +using Umbraco.Web; + +namespace Umbraco.Tests.TestHelpers.ControllerTesting +{ + public class TestControllerActivator : TestControllerActivatorBase + { + private readonly Func _factory; + + public TestControllerActivator(Func factory) + { + _factory = factory; + } + + protected override ApiController CreateController(Type controllerType, HttpRequestMessage msg, UmbracoHelper helper) + { + return _factory(msg, helper); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs new file mode 100644 index 0000000000..bebf6ab1e2 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -0,0 +1,140 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Web; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Dispatcher; +using System.Web.Security; +using Moq; +using Semver; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Dictionary; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Profiling; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; +using Umbraco.Web.WebApi; + +namespace Umbraco.Tests.TestHelpers.ControllerTesting +{ + /// + /// Used to mock all of the services required for re-mocking and testing controllers + /// + /// + /// A more complete version of this is found in the Umbraco REST API project but this has the basics covered + /// + public abstract class TestControllerActivatorBase : DefaultHttpControllerActivator, IHttpControllerActivator + { + IHttpController IHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) + { + if (typeof(UmbracoApiControllerBase).IsAssignableFrom(controllerType)) + { + var owinContext = request.GetOwinContext(); + + var mockedUserService = Mock.Of(); + + var mockedMigrationService = new Mock(); + //set it up to return anything so that the app ctx is 'Configured' + mockedMigrationService.Setup(x => x.FindEntry(It.IsAny(), It.IsAny())).Returns(Mock.Of()); + + var serviceContext = new ServiceContext( + userService: mockedUserService, + migrationEntryService: mockedMigrationService.Object); + + //ensure the configuration matches the current version for tests + SettingsForTests.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToSemanticString(); + + //new app context + var dbCtx = new Mock(Mock.Of(), Mock.Of(), Mock.Of(), "test"); + //ensure these are set so that the appctx is 'Configured' + dbCtx.Setup(x => x.CanConnect).Returns(true); + dbCtx.Setup(x => x.IsDatabaseConfigured).Returns(true); + var appCtx = ApplicationContext.EnsureContext( + dbCtx.Object, + //pass in mocked services + serviceContext, + CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of()), + true); + + //httpcontext with an auth'd user + var httpContext = Mock.Of( + http => http.User == owinContext.Authentication.User + //ensure the request exists with a cookies collection + && http.Request == Mock.Of(r => r.Cookies == new HttpCookieCollection())); + //chuck it into the props since this is what MS does when hosted and it's needed there + request.Properties["MS_HttpContext"] = httpContext; + + var backofficeIdentity = (UmbracoBackOfficeIdentity)owinContext.Authentication.User.Identity; + + var webSecurity = new Mock(null, null); + + //mock CurrentUser + webSecurity.Setup(x => x.CurrentUser) + .Returns(Mock.Of(u => u.IsApproved == true + && u.IsLockedOut == false + && u.AllowedSections == backofficeIdentity.AllowedApplications + && u.Email == "admin@admin.com" + && u.Id == (int) backofficeIdentity.Id + && u.Language == "en" + && u.Name == backofficeIdentity.RealName + && u.StartContentId == backofficeIdentity.StartContentNode + && u.StartMediaId == backofficeIdentity.StartMediaNode + && u.Username == backofficeIdentity.Username)); + + //mock Validate + webSecurity.Setup(x => x.ValidateCurrentUser()) + .Returns(() => true); + webSecurity.Setup(x => x.UserHasAppAccess(It.IsAny(), It.IsAny())) + .Returns(() => true); + + var umbCtx = UmbracoContext.EnsureContext( + //set the user of the HttpContext + httpContext, + appCtx, + webSecurity.Object, + Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == UrlProviderMode.Auto.ToString())), + Enumerable.Empty(), + true); //replace it + + var urlHelper = new Mock(); + urlHelper.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns("/hello/world/1234"); + + var membershipHelper = new MembershipHelper(umbCtx, Mock.Of(), Mock.Of()); + + var mockedTypedContent = Mock.Of(); + + var umbHelper = new UmbracoHelper(umbCtx, + Mock.Of(), + mockedTypedContent, + Mock.Of(), + Mock.Of(), + Mock.Of(), + new UrlProvider(umbCtx, new[] + { + urlHelper.Object + }, UrlProviderMode.Auto), + Mock.Of(), + Mock.Of(), + membershipHelper); + + return CreateController(controllerType, request, umbHelper); + } + //default + return base.Create(request, controllerDescriptor, controllerType); + } + + protected abstract ApiController CreateController(Type controllerType, HttpRequestMessage msg, UmbracoHelper helper); + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs new file mode 100644 index 0000000000..12ffda5e04 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs @@ -0,0 +1,76 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Web.Http; +using Microsoft.Owin.Testing; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Web; +using Umbraco.Web.WebApi; + +namespace Umbraco.Tests.TestHelpers.ControllerTesting +{ + public class TestRunner + { + private readonly Func _controllerFactory; + + public TestRunner(Func controllerFactory) + { + _controllerFactory = controllerFactory; + } + + public async Task> Execute(string controllerName, string actionName, HttpMethod method) + { + var startup = new TestStartup( + configuration => + { + configuration.Routes.MapHttpRoute("Default", + routeTemplate: "{controller}/{action}/{id}", + defaults: new { controller = controllerName, action = actionName, id = RouteParameter.Optional }); + }, + _controllerFactory); + + using (var server = TestServer.Create(builder => startup.Configuration(builder))) + { + var request = new HttpRequestMessage + { + RequestUri = new Uri("http://testserver/"), + Method = method, + }; + + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + Console.WriteLine(request); + var response = await server.HttpClient.SendAsync(request); + Console.WriteLine(response); + + string json = ""; + if (response.IsSuccessStatusCode == false) + { + WriteResponseError(response); + } + else + { + json = (await ((StreamContent)response.Content).ReadAsStringAsync()).TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + var deserialized = JsonConvert.DeserializeObject(json); + Console.Write(JsonConvert.SerializeObject(deserialized, Formatting.Indented)); + } + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + return Tuple.Create(response, json); + } + } + + private static void WriteResponseError(HttpResponseMessage response) + { + var result = response.Content.ReadAsStringAsync().Result; + Console.Out.WriteLine("Http operation unsuccessfull"); + Console.Out.WriteLine(string.Format("Status: '{0}'", response.StatusCode)); + Console.Out.WriteLine(string.Format("Reason: '{0}'", response.ReasonPhrase)); + Console.Out.WriteLine(result); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestStartup.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestStartup.cs new file mode 100644 index 0000000000..1bbb03095f --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestStartup.cs @@ -0,0 +1,54 @@ +using System; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Dispatcher; +using System.Web.Http.ExceptionHandling; +using System.Web.Http.Tracing; +using Owin; +using Umbraco.Web; +using Umbraco.Web.Editors; +using Umbraco.Web.WebApi; + +namespace Umbraco.Tests.TestHelpers.ControllerTesting +{ + /// + /// Startup class for the self-hosted web server works for OWIN and WebAPI + /// + public class TestStartup + { + private readonly Func _controllerFactory; + private readonly Action _initialize; + + public TestStartup(Action initialize, Func controllerFactory) + { + _controllerFactory = controllerFactory; + _initialize = initialize; + } + + public void Configuration(IAppBuilder app) + { + var httpConfig = new HttpConfiguration(); + + //TODO: Enable this if you can't see the errors produced + //var traceWriter = httpConfig.EnableSystemDiagnosticsTracing(); + //traceWriter.IsVerbose = true; + //traceWriter.MinimumLevel = TraceLevel.Debug; + + httpConfig.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; + + // Add in a simple exception tracer so we can see what is causing the 500 Internal Server Error + httpConfig.Services.Add(typeof(IExceptionLogger), new TraceExceptionLogger()); + + httpConfig.Services.Replace(typeof(IAssembliesResolver), new SpecificAssemblyResolver(new[] { typeof(UsersController).Assembly })); + httpConfig.Services.Replace(typeof(IHttpControllerActivator), new TestControllerActivator(_controllerFactory)); + httpConfig.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(httpConfig)); + + //auth everything + app.AuthenticateEverything(); + + _initialize(httpConfig); + + app.UseWebApi(httpConfig); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TraceExceptionLogger.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TraceExceptionLogger.cs new file mode 100644 index 0000000000..faf5870cbb --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TraceExceptionLogger.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; +using System.Web.Http.ExceptionHandling; + +namespace Umbraco.Tests.TestHelpers.ControllerTesting +{ + /// + /// Traces any errors for WebApi to the output window + /// + public class TraceExceptionLogger : ExceptionLogger + { + public override void Log(ExceptionLoggerContext context) + { + Trace.TraceError(context.ExceptionContext.Exception.ToString()); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 498bc35c86..651ae301ce 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -76,8 +76,23 @@ ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - - ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + + ..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Host.HttpListener.3.1.0\lib\net45\Microsoft.Owin.Host.HttpListener.dll + + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + ..\packages\Microsoft.Owin.Hosting.3.1.0\lib\net45\Microsoft.Owin.Hosting.dll + + + ..\packages\Microsoft.Owin.Security.3.1.0\lib\net45\Microsoft.Owin.Security.dll + + + ..\packages\Microsoft.Owin.Testing.3.1.0\lib\net45\Microsoft.Owin.Testing.dll ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -128,9 +143,14 @@ ..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll - - ..\packages\Microsoft.AspNet.WebApi.SelfHost.4.0.30506.0\lib\net40\System.Web.Http.SelfHost.dll - True + + ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.3\lib\net45\System.Web.Http.Owin.dll + + + ..\packages\Microsoft.AspNet.WebApi.SelfHost.5.2.3\lib\net45\System.Web.Http.SelfHost.dll + + + ..\packages\Microsoft.AspNet.WebApi.Tracing.5.2.3\lib\net45\System.Web.Http.Tracing.dll ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll @@ -162,6 +182,15 @@ + + + + + + + + + diff --git a/src/Umbraco.Tests/packages.config b/src/Umbraco.Tests/packages.config index a9cbce1aad..1577212ffe 100644 --- a/src/Umbraco.Tests/packages.config +++ b/src/Umbraco.Tests/packages.config @@ -1,6 +1,5 @@  - @@ -12,10 +11,18 @@ - + + + + - + + + + + + diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 0d48b96bd0..e34d98e902 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -37,7 +37,7 @@ namespace Umbraco.Web.Editors : base(umbracoContext) { } - + /// /// Gets a user by Id /// @@ -55,16 +55,21 @@ namespace Umbraco.Web.Editors public PagedResult GetPagedUsers( int pageNumber = 1, - int pageSize = 0, + int pageSize = 10, string orderBy = "username", Direction orderDirection = Direction.Ascending, - string[] userGroups = null, + [FromUri]string[] userGroups = null, string filter = "") { long pageIndex = pageNumber - 1; long total; var result = Services.UserService.GetAll(pageIndex, pageSize, out total, orderBy, orderDirection, null, userGroups, filter); + if (total == 0) + { + return new PagedResult(0, 0, 0); + } + return new PagedResult(total, pageNumber, pageSize) { Items = Mapper.Map>(result) diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index e459bb9eb4..05407a93e5 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -395,7 +395,7 @@ namespace Umbraco.Web.Security /// /// /// - internal bool UserHasAppAccess(string app, IUser user) + internal virtual bool UserHasAppAccess(string app, IUser user) { var apps = user.AllowedSections; return apps.Any(uApp => uApp.InvariantEquals(app)); diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 90f4e4df03..dbb165a526 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -259,7 +259,7 @@ namespace Umbraco.Web var requestUrl = new Uri("http://localhost"); var request = GetRequestFromContext(); - if (request != null) + if (request != null && request.Url != null) { requestUrl = request.Url; } diff --git a/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs b/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs index 66a79376aa..8dca204597 100644 --- a/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs +++ b/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs @@ -21,6 +21,8 @@ namespace Umbraco.Web.WebApi public class AngularJsonMediaTypeFormatter : JsonMediaTypeFormatter { + public const string XsrfPrefix = ")]}',\n"; + /// /// This will prepend the special chars to the stream output that angular will strip /// @@ -30,24 +32,25 @@ namespace Umbraco.Web.WebApi /// /// /// - public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) + public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) { - if (type == null) throw new ArgumentNullException("type"); if (writeStream == null) throw new ArgumentNullException("writeStream"); var effectiveEncoding = SelectCharacterEncoding(content == null ? null : content.Headers); - using (var streamWriter = new StreamWriter(writeStream, effectiveEncoding)) + using (var streamWriter = new StreamWriter(writeStream, effectiveEncoding, + //we are only writing a few chars so we don't need to allocate a large buffer + 128, + //this is important! We don't want to close the stream, the base class is in charge of stream management, we just want to write to it. + leaveOpen:true)) { //write the special encoding for angular json to the start // (see: http://docs.angularjs.org/api/ng.$http) - streamWriter.Write(")]}',\n"); + streamWriter.Write(XsrfPrefix); streamWriter.Flush(); - return base.WriteToStreamAsync(type, value, writeStream, content, transportContext); + await base.WriteToStreamAsync(type, value, writeStream, content, transportContext); } - - } } diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs index 23e0fde1c2..c305ad3339 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs @@ -33,10 +33,12 @@ namespace Umbraco.Web.WebApi.Filters { return true; } - - return UmbracoContext.Current.Security.CurrentUser != null + + var authorized = UmbracoContext.Current.Security.CurrentUser != null && _appNames.Any(app => UmbracoContext.Current.Security.UserHasAppAccess( app, UmbracoContext.Current.Security.CurrentUser)); + + return authorized; } } } \ No newline at end of file From e590b20e7508a2cf52ec88c3c929c599c3c497e4 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 19 May 2017 13:12:53 +0200 Subject: [PATCH 077/714] remove some strokes and borders --- .../src/less/components/users/umb-users.less | 11 +--- .../src/views/users/views/users/users.html | 53 ++++++++++--------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less index 7d4dde9e0c..9e0e2c65f0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-users.less @@ -57,16 +57,7 @@ .umb-user__group { font-size: 14px; text-align: center; -} - -.umb-user__divider { - display: block; - width: 20px; - border-bottom: 1px solid @gray-7; - margin-top: 15px; - margin-bottom: 10px; - margin-left: auto; - margin-right: auto; + margin-bottom: 15px; } .umb-user__last-login { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 5f0ad74e29..ae4f917145 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -61,30 +61,36 @@ - + - - + +
+
+ - - + +
+
+ - + label="Disable" + icon="icon-block" + action="vm.disableUser()"> + +
@@ -100,7 +106,7 @@
[DataContract(Name = "user", Namespace = "")] + [ReadOnly(true)] public class UserDisplay : EntityBasic, INotificationModel { public UserDisplay() @@ -16,6 +19,21 @@ namespace Umbraco.Web.Models.ContentEditing Notifications = new List(); } + /// + /// The MD5 lowercase hash of the email which can be used by gravatar + /// + [DataMember(Name = "emailHash")] + public string EmailHash { get; set; } + + [DataMember(Name = "lastLoginDate")] + public DateTime LastLoginDate { get; set; } + + [DataMember(Name = "customAvatar")] + public string CustomAvatar { get; set; } + + [DataMember(Name = "userState")] + public UserState UserState { get; set; } + [DataMember(Name = "culture", IsRequired = true)] public string Culture { get; set; } @@ -33,7 +51,6 @@ namespace Umbraco.Web.Models.ContentEditing /// The key is the Alias the value is the Name - the Alias is what is used in the UserGroup property and for persistence ///
[DataMember(Name = "availableUserGroups")] - [ReadOnly(true)] public IDictionary AvailableUserGroups { get; set; } /// @@ -41,7 +58,6 @@ namespace Umbraco.Web.Models.ContentEditing /// The key is the culture stored in the database, the value is the Name /// [DataMember(Name = "availableCultures")] - [ReadOnly(true)] public IDictionary AvailableCultures { get; set; } [DataMember(Name = "startContentId")] @@ -61,7 +77,6 @@ namespace Umbraco.Web.Models.ContentEditing /// The key is the Alias the value is the Name - the Alias is what is used in the AllowedSections property and for persistence ///
[DataMember(Name = "availableSections")] - [ReadOnly(true)] public IDictionary AvailableSections { get; set; } /// diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index f58dfe875a..00e8e9a230 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -31,14 +31,19 @@ namespace Umbraco.Web.Models.Mapping .ForMember( detail => detail.AvailableCultures, opt => opt.MapFrom(user => applicationContext.Services.TextService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName))) + .ForMember( + detail => detail.EmailHash, + opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) .ForMember(detail => detail.ParentId, opt => opt.UseValue(-1)) - .ForMember(detail => detail.Path, opt => opt.MapFrom(user => "-1," + user.Id)) + .ForMember(detail => detail.Path, opt => opt.MapFrom(user => "-1," + user.Id)) .ForMember(detail => detail.Notifications, opt => opt.Ignore()) .ForMember(detail => detail.Udi, opt => opt.Ignore()) .ForMember(detail => detail.Icon, opt => opt.Ignore()) .ForMember(detail => detail.Trashed, opt => opt.Ignore()) .ForMember(detail => detail.Alias, opt => opt.Ignore()) .ForMember(detail => detail.Trashed, opt => opt.Ignore()) + //TODO: Enable this when we can! + .ForMember(detail => detail.CustomAvatar, opt => opt.Ignore()) .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()); config.CreateMap() From 16c6fc70bffb07033f547c03a8381327ec6722a4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 22 May 2017 21:26:46 +1000 Subject: [PATCH 081/714] updates user resource to use the real endpoint --- .../src/common/resources/users.resource.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 4c1f7fa1a5..33766bb675 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -71,14 +71,16 @@ 'Failed to retrieve users paged result'); } - function getUser() { - var deferred = $q.defer(); - var user = { - "name": "Tammy Contreras", - "email": "tammy@contreras.com" - }; - deferred.resolve(user); - return deferred.promise; + function getUser(userId) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "GetById", + { id: id })), + "Failed to retrieve data for user " + id); + } function getUsers() { From 83d26fd91a9c5615c483d0e8159c1e3eb31cfb97 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 22 May 2017 14:25:29 +0200 Subject: [PATCH 082/714] add breadcrumb + back button to editor footer --- .../src/views/users/role.controller.js | 20 ++++- .../src/views/users/role.html | 23 ++++-- .../src/views/users/user.controller.js | 21 +++++- .../src/views/users/user.html | 73 +++++++++++-------- 4 files changed, 93 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js index b44103e726..12f23d3f7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js @@ -9,7 +9,7 @@ vm.page = {}; vm.userRole = {}; - vm.goBack = goBack; + vm.goToPage = goToPage; vm.openSectionPicker = openSectionPicker; vm.openContentPicker = openContentPicker; vm.openMediaPicker = openMediaPicker; @@ -23,6 +23,7 @@ // get user usersResource.getUserRole().then(function (userRole) { vm.userRole = userRole; + makeBreadcrumbs(); }); // fake loading @@ -32,8 +33,8 @@ } - function goBack() { - $location.path("/users/users/overview").search("subview", "roles"); + function goToPage(ancestor) { + $location.path(ancestor.path).search("subview", ancestor.subView); } function openSectionPicker() { @@ -97,6 +98,19 @@ return "success"; } } + + function makeBreadcrumbs() { + vm.breadcrumbs = [ + { + "name": "Groups", + "path": "/users/users/overview", + "subView": "roles" + }, + { + "name": vm.userRole.name + } + ]; + } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/role.html b/src/Umbraco.Web.UI.Client/src/views/users/role.html index 54cc1abebb..d8c79c881e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/role.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/role.html @@ -15,12 +15,6 @@ - - - ← Back to roles - - -
@@ -138,8 +132,25 @@ + + + + + + + + + + - - - ← Back to users - - -
@@ -172,40 +166,38 @@
{{ vm.user.createdBy }}
-
+
+
+ + +
-
+
+ + +
-
-
- - -
- - - -
@@ -218,8 +210,25 @@ + + + + + + + + + + Date: Mon, 22 May 2017 14:25:57 +0200 Subject: [PATCH 083/714] increase breadcrumb font size --- .../src/less/components/umb-breadcrumbs.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less index 2cad03a2ab..d25744a108 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -12,7 +12,7 @@ .umb-breadcrumbs__ancestor-link, .umb-breadcrumbs__ancestor-text { - font-size: 12px; + font-size: 13px; color: @gray-3; max-width: 150px; white-space: nowrap; From 86e4acef86614cfa6a2deeeec48046add2937318 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 23 May 2017 12:13:01 +1000 Subject: [PATCH 084/714] passes through the user id for the user editor --- .../src/common/resources/users.resource.js | 4 ++-- src/Umbraco.Web.UI.Client/src/views/users/user.controller.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 33766bb675..e460341c77 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -78,8 +78,8 @@ umbRequestHelper.getApiUrl( "userApiBaseUrl", "GetById", - { id: id })), - "Failed to retrieve data for user " + id); + { id: userId })), + "Failed to retrieve data for user " + userId); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index fd3950bde6..1ac45d2971 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, $timeout, $location, usersResource) { + function UserEditController($scope, $timeout, $location, usersResource, $routeParams) { var vm = this; @@ -16,7 +16,7 @@ vm.loading = true; // get user - usersResource.getUser().then(function (user) { + usersResource.getUser($routeParams.id).then(function (user) { vm.user = user; }); From 1be3f06bc49b95039a65eb75c8748c4b00964687 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 23 May 2017 12:28:41 +1000 Subject: [PATCH 085/714] Hides the user invite button if there is no email server configured --- .../views/users/views/users/users.controller.js | 13 ++++++++----- src/Umbraco.Web/Editors/BackOfficeController.cs | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 3c8400e2b0..10db1fb908 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -37,12 +37,15 @@ "selected": true }; - vm.defaultButton = { - labelKey:"users_inviteUser", - handler: function() { - vm.setUsersViewState('inviteUser'); + //don't set this if no email is configured + if (Umbraco.Sys.ServerVariables.umbracoSettings.emailServerConfigured) { + vm.defaultButton = { + labelKey: "users_inviteUser", + handler: function () { + vm.setUsersViewState('inviteUser'); } - }; + }; + } vm.subButtons = [ { diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index bb37b22235..e75d7fc321 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Configuration; using System.Net.Http; using System.Text; using System.Threading; @@ -400,6 +401,7 @@ namespace Umbraco.Web.Editors {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')}, {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset}, {"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage}, + {"emailServerConfigured", HasSmtpServerConfigured()}, } }, { @@ -447,6 +449,18 @@ namespace Umbraco.Web.Editors return JavaScript(result); } + private bool HasSmtpServerConfigured() + { + var config = WebConfigurationManager.OpenWebConfiguration(HttpContext.Request.ApplicationPath); + var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings"); + if (settings == null || settings.Smtp == null) return false; + if (settings.Smtp.SpecifiedPickupDirectory != null && string.IsNullOrEmpty(settings.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation) == false) + return true; + if (settings.Smtp.Network != null && string.IsNullOrEmpty(settings.Smtp.Network.Host) == false) + return true; + return false; + } + [HttpPost] public ActionResult ExternalLogin(string provider, string redirectUrl = null) { From 6e9e2b99a4e3d312ddc1b8ab71f1b86dd3971c28 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 23 May 2017 13:18:29 +1000 Subject: [PATCH 086/714] U4-9944 User table needs to have created/update dates , adds more methods to the user controller for enabling/disabling users --- src/Umbraco.Core/Models/Rdbms/UserDto.cs | 11 +++++ src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs | 14 +++++- .../Persistence/Factories/UserFactory.cs | 14 +++--- .../Persistence/Factories/UserGroupFactory.cs | 4 ++ .../Persistence/Mappers/UserMapper.cs | 5 ++ .../Migrations/Initial/BaseDataCreation.cs | 10 ++-- .../AddUserGroupTables.cs | 22 ++++++--- .../UpdateUserTables.cs | 31 +++++++++++++ .../DataTypeDefinitionRepository.cs | 2 +- .../Repositories/UserGroupRepository.cs | 4 ++ .../Repositories/UserRepository.cs | 15 +++--- src/Umbraco.Core/Services/IUserService.cs | 9 +++- src/Umbraco.Core/Services/UserService.cs | 19 +++++--- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../ControllerTesting/TestRunner.cs | 2 +- .../Editors/BackOfficeController.cs | 2 +- src/Umbraco.Web/Editors/UsersController.cs | 46 +++++++++++++++---- 17 files changed, 167 insertions(+), 44 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 11624e25f5..5d2257d01b 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Models.Rdbms { @@ -75,6 +76,16 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastLoginDate { get; set; } + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + [ResultColumn] public List UserGroupDtos { get; set; } } diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs index cadf7cbcdb..cd700dee68 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Models.Rdbms { @@ -33,6 +35,16 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] public string DefaultPermissions { get; set; } + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + [ResultColumn] public List UserGroup2AppDtos { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index ea30773757..81a4b88289 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -6,10 +6,8 @@ using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Factories { - internal class UserFactory + internal static class UserFactory { - #region Implementation of IEntityFactory - public static IUser BuildEntity(UserDto dto) { var guidId = dto.Id.ToGuid(); @@ -43,6 +41,8 @@ namespace Umbraco.Core.Persistence.Factories user.LastLockoutDate = dto.LastLockoutDate ?? DateTime.MinValue; user.LastLoginDate = dto.LastLoginDate ?? DateTime.MinValue; user.LastPasswordChangeDate = dto.LastPasswordChangeDate ?? DateTime.MinValue; + user.CreateDate = dto.CreateDate; + user.UpdateDate = dto.UpdateDate; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -56,7 +56,7 @@ namespace Umbraco.Core.Persistence.Factories } } - public UserDto BuildDto(IUser entity) + public static UserDto BuildDto(IUser entity) { var dto = new UserDto { @@ -74,6 +74,8 @@ namespace Umbraco.Core.Persistence.Factories LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? (DateTime?)null : entity.LastLockoutDate, LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? (DateTime?)null : entity.LastLoginDate, LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate }; if (entity.HasIdentity) @@ -82,8 +84,6 @@ namespace Umbraco.Core.Persistence.Factories } return dto; - } - - #endregion + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs index 0ca2f6cb8c..cdbc153814 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs @@ -22,6 +22,8 @@ namespace Umbraco.Core.Persistence.Factories userGroup.Permissions = dto.DefaultPermissions.IsNullOrWhiteSpace() ? Enumerable.Empty() : dto.DefaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); + userGroup.CreateDate = dto.CreateDate; + userGroup.UpdateDate = dto.UpdateDate; if (dto.UserGroup2AppDtos != null) { @@ -48,6 +50,8 @@ namespace Umbraco.Core.Persistence.Factories DefaultPermissions = entity.Permissions == null ? "" : string.Join("", entity.Permissions), Name = entity.Name, UserGroup2AppDtos = new List(), + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate }; foreach (var app in entity.AllowedSections) diff --git a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs index bf7ca9ed8f..9ea69abbf6 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs @@ -40,6 +40,11 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.IsApproved, dto => dto.Disabled); CacheMap(src => src.IsLockedOut, dto => dto.NoConsole); CacheMap(src => src.Language, dto => dto.UserLanguage); + CacheMap(src => src.CreateDate, dto => dto.CreateDate); + CacheMap(src => src.UpdateDate, dto => dto.UpdateDate); + CacheMap(src => src.LastLockoutDate, dto => dto.LastLockoutDate); + CacheMap(src => src.LastLoginDate, dto => dto.LastLoginDate); + CacheMap(src => src.LastPasswordChangeDate, dto => dto.LastPasswordChangeDate); } #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 90768d2153..434a011a28 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -169,16 +169,16 @@ namespace Umbraco.Core.Persistence.Migrations.Initial private void CreateUmbracoUserData() { - _database.Insert("umbracoUser", "id", false, new UserDto { Id = 0, Disabled = false, NoConsole = false, ContentStartId = -1, MediaStartId = -1, UserName = "Administrator", Login = "admin", Password = "default", Email = "", UserLanguage = "en" }); + _database.Insert("umbracoUser", "id", false, new UserDto { Id = 0, Disabled = false, NoConsole = false, ContentStartId = -1, MediaStartId = -1, UserName = "Administrator", Login = "admin", Password = "default", Email = "", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); //_database.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); } private void CreateUmbracoUserGroupData() { - _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 1, Alias = Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7" }); - _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 2, Alias = "writer", Name = "Writers", DefaultPermissions = "CAH:F" }); - _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 3, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5F" }); - _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 4, Alias = "translator", Name = "Translators", DefaultPermissions = "AF" }); + _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 1, Alias = Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); + _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 2, Alias = "writer", Name = "Writers", DefaultPermissions = "CAH:F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); + _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 3, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); + _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 4, Alias = "translator", Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); } private void CreateUmbracoUser2UserGroupData() diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index f4b5de6c02..1d2a7a3d9a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero { - [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + [Migration("7.7.0", 1, Constants.System.UmbracoMigrationName)] public class AddUserGroupTables : MigrationBase { public AddUserGroupTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) @@ -20,33 +20,41 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); - AddNewTables(tables); - MigrateUserPermissions(); - MigrateUserTypesToGroups(); - DeleteOldTables(tables, constraints); + if (AddNewTables(tables)) + { + MigrateUserPermissions(); + MigrateUserTypesToGroups(); + DeleteOldTables(tables, constraints); + } } - private void AddNewTables(string[] tables) + private bool AddNewTables(string[] tables) { + var updated = false; if (tables.InvariantContains("umbracoUserGroup") == false) { Create.Table(); + updated = true; } if (tables.InvariantContains("umbracoUser2UserGroup") == false) { Create.Table(); + updated = true; } if (tables.InvariantContains("umbracoUserGroup2App") == false) { - Create.Table(); + Create.Table(); + updated = true; } if (tables.InvariantContains("umbracoUserGroup2NodePermission") == false) { Create.Table(); + updated = true; } + return updated; } private void MigrateUserTypesToGroups() diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs new file mode 100644 index 0000000000..04629e74c3 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero +{ + [Migration("7.7.0", 0, Constants.System.UmbracoMigrationName)] + public class UpdateUserTables : MigrationBase + { + public UpdateUserTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) + { + } + + public override void Up() + { + //Don't exeucte if the column is already there + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("createDate")) == false) + Create.Column("createDate").OnTable("umbracoUser").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime); + + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("updateDate")) == false) + Create.Column("updateDate").OnTable("umbracoUser").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 6fd4c8f906..8c263f4211 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -208,7 +208,7 @@ AND umbracoNode.id <> @id", throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); } - //Updates Modified date and Version Guid + //Updates Modified date ((DataTypeDefinition)entity).UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index 58a4e90b5d..727f75f96e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -226,6 +226,8 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistNewItem(IUserGroup entity) { + ((UserGroup)entity).AddingEntity(); + var userGroupDto = UserGroupFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(userGroupDto)); @@ -236,6 +238,8 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistUpdatedItem(IUserGroup entity) { + ((UserGroup)entity).UpdatingEntity(); + var userGroupDto = UserGroupFactory.BuildDto(entity); Database.Update(userGroupDto); diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 4ad7ecb390..4c203f6dbb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -160,15 +160,15 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistNewItem(IUser entity) { - var userFactory = new UserFactory(); - + ((User)entity).AddingEntity(); + //ensure security stamp if non if (entity.SecurityStamp.IsNullOrWhiteSpace()) { entity.SecurityStamp = Guid.NewGuid().ToString(); } - var userDto = userFactory.BuildDto(entity); + var userDto = UserFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(userDto)); entity.Id = id; @@ -196,15 +196,16 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistUpdatedItem(IUser entity) { - var userFactory = new UserFactory(); - + //Updates Modified date + ((User)entity).UpdatingEntity(); + //ensure security stamp if non if (entity.SecurityStamp.IsNullOrWhiteSpace()) { entity.SecurityStamp = Guid.NewGuid().ToString(); } - var userDto = userFactory.BuildDto(entity); + var userDto = UserFactory.BuildDto(entity); //build list of columns to check for saving - we don't want to save the password if it hasn't changed! //List the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that @@ -224,6 +225,8 @@ namespace Umbraco.Core.Persistence.Repositories {"lastPasswordChangeDate", "LastPasswordChangeDate"}, {"lastLoginDate", "LastLoginDate"}, {"failedLoginAttempts", "FailedPasswordAttempts"}, + {"createDate", "CreateDate"}, + {"updateDate", "UpdateDate"} }; //create list of properties that have changed diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index ddaf44e6d9..878fef654d 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -51,7 +51,14 @@ namespace Umbraco.Core.Services /// Id of the user to retrieve /// IUser GetUserById(int id); - + + /// + /// Gets a users by Id + /// + /// Ids of the users to retrieve + /// + IEnumerable GetUsersById(params int[] ids); + /// /// Removes a specific section from all user groups /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 156e52f9a8..9bba28ee62 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -196,12 +196,8 @@ namespace Umbraco.Core.Services public void Delete(IUser membershipUser) { //disable - membershipUser.IsApproved = false; - //can't rename if it's going to take up too many chars - if (membershipUser.Username.Length + 9 <= 125) - { - membershipUser.Username = DateTime.Now.ToString("yyyyMMdd") + "_" + membershipUser.Username; - } + membershipUser.IsApproved = false; + Save(membershipUser); } @@ -637,6 +633,17 @@ namespace Umbraco.Core.Services } } + public IEnumerable GetUsersById(params int[] ids) + { + if (ids.Length <= 0) return Enumerable.Empty(); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateUserRepository(uow); + return repository.GetAll(ids); + } + } + /// /// Replaces the same permission set for a single group to any number of entities /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 01329ab6eb..b6728affdf 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -510,6 +510,7 @@ + diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs index 12ffda5e04..32e3a08e31 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs @@ -37,7 +37,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting { var request = new HttpRequestMessage { - RequestUri = new Uri("http://testserver/"), + RequestUri = new Uri("https://testserver/"), Method = method, }; diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index e75d7fc321..fd70d71c99 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -239,7 +239,7 @@ namespace Umbraco.Web.Editors }, { "userApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.PostDisableUser(0)) + controller => controller.PostSaveUser(null)) }, { "contentApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index e34d98e902..575e354876 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -53,6 +53,16 @@ namespace Umbraco.Web.Editors return Mapper.Map(user); } + /// + /// Returns a paged users collection + /// + /// + /// + /// + /// + /// + /// + /// public PagedResult GetPagedUsers( int pageNumber = 1, int pageSize = 10, @@ -76,6 +86,11 @@ namespace Umbraco.Web.Editors }; } + /// + /// Saves a user + /// + /// + /// public UserDisplay PostSaveUser(UserSave userSave) { if (userSave == null) throw new ArgumentNullException("userSave"); @@ -98,18 +113,33 @@ namespace Umbraco.Web.Editors } /// - /// Disables the user with the given user id + /// Disables the users with the given user ids /// - /// - public bool PostDisableUser([FromUri]int userId) + /// + public bool PostDisableUsers([FromUri]int[] userIds) { - var user = Services.UserService.GetUserById(userId); - if (user == null) + var users = Services.UserService.GetUsersById(userIds); + foreach (var u in users) { - throw new HttpResponseException(HttpStatusCode.NotFound); + //without the permanent flag, this will just disable + Services.UserService.Delete(u); } - //without the permanent flag, this will just disable - Services.UserService.Delete(user); + return true; + } + + /// + /// Enables the users with the given user ids + /// + /// + public bool PostEnableUsers([FromUri]int[] userIds) + { + var users = Services.UserService.GetUsersById(userIds).ToArray(); + foreach (var u in users) + { + u.IsApproved = true; + } + Services.UserService.Save(users); + return true; } } From 45fa106c4281f04cc91cb4815ade6c38f8b421ce Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 23 May 2017 15:23:10 +1000 Subject: [PATCH 087/714] Adding required user group db columns and models, fixing tests --- src/Umbraco.Core/Models/EntityBase/Entity.cs | 6 +- .../Models/Membership/IUserGroup.cs | 8 +++ .../Models/Membership/UserGroup.cs | 27 +++++++++ src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs | 4 ++ .../Persistence/Factories/UserGroupFactory.cs | 4 +- src/Umbraco.Core/Services/IUserService.cs | 8 --- src/Umbraco.Core/Services/UserService.cs | 34 +---------- .../NotificationsRepositoryTest.cs | 8 +-- .../Services/UserServiceTests.cs | 58 ++++++++++--------- src/Umbraco.Web/Editors/UsersController.cs | 16 ++++- .../Models/ContentEditing/UserDisplay.cs | 4 +- .../Models/ContentEditing/UserGroupDisplay.cs | 29 ++++++++++ .../Models/Mapping/UserModelMapper.cs | 5 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 14 files changed, 132 insertions(+), 80 deletions(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/UserGroupDisplay.cs diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index d4da2676c1..3e7770088b 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -119,7 +119,7 @@ namespace Umbraco.Core.Models.EntityBase if (IsPropertyDirty("CreateDate") == false || _createDate == default(DateTime)) CreateDate = DateTime.Now; if (IsPropertyDirty("UpdateDate") == false || _updateDate == default(DateTime)) - UpdateDate = CreateDate; + UpdateDate = DateTime.Now; } /// @@ -129,6 +129,10 @@ namespace Umbraco.Core.Models.EntityBase { if (IsPropertyDirty("UpdateDate") == false || _updateDate == default(DateTime)) UpdateDate = DateTime.Now; + + //this is just in case + if (_createDate == default(DateTime)) + CreateDate = DateTime.Now; } /// diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index 336caa4c47..d2c58883a2 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -5,6 +5,14 @@ namespace Umbraco.Core.Models.Membership { public interface IUserGroup : IAggregateRoot { + int StartContentId { get; set; } + int StartMediaId { get; set; } + + /// + /// The icon + /// + string Icon { get; set; } + /// /// The alias /// diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 885a7cbe55..b008f4652f 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -14,7 +14,10 @@ namespace Umbraco.Core.Models.Membership [DataContract(IsReference = true)] internal class UserGroup : Entity, IUserGroup { + private int _startContentId; + private int _startMediaId; private string _alias; + private string _icon; private string _name; private IEnumerable _permissions; private readonly List _sectionCollection; @@ -26,6 +29,9 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); public readonly PropertyInfo PermissionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Permissions); + public readonly PropertyInfo IconSelector = ExpressionHelper.GetPropertyInfo(x => x.Icon); + public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentId); + public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); } public UserGroup() @@ -33,6 +39,27 @@ namespace Umbraco.Core.Models.Membership _sectionCollection = new List(); } + [DataMember] + public int StartMediaId + { + get { return _startMediaId; } + set { SetPropertyValueAndDetectChanges(value, ref _startMediaId, Ps.Value.StartMediaIdSelector); } + } + + [DataMember] + public int StartContentId + { + get { return _startContentId; } + set { SetPropertyValueAndDetectChanges(value, ref _startContentId, Ps.Value.StartContentIdSelector); } + } + + [DataMember] + public string Icon + { + get { return _icon; } + set { SetPropertyValueAndDetectChanges(value, ref _icon, Ps.Value.IconSelector); } + } + [DataMember] public string Alias { diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs index cd700dee68..77f1fed7f1 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -45,6 +45,10 @@ namespace Umbraco.Core.Models.Rdbms [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } + [Column("icon")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Icon { get; set; } + [ResultColumn] public List UserGroup2AppDtos { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs index cdbc153814..a9cbf95c77 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Factories : dto.DefaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); userGroup.CreateDate = dto.CreateDate; userGroup.UpdateDate = dto.UpdateDate; + userGroup.Icon = dto.Icon; if (dto.UserGroup2AppDtos != null) { @@ -51,7 +52,8 @@ namespace Umbraco.Core.Persistence.Factories Name = entity.Name, UserGroup2AppDtos = new List(), CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate + UpdateDate = entity.UpdateDate, + Icon = entity.Icon }; foreach (var app in entity.AllowedSections) diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 878fef654d..6b798862b0 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -65,14 +65,6 @@ namespace Umbraco.Core.Services /// This is useful when an entire section is removed from config /// Alias of the section to remove void DeleteSectionFromAllUserGroups(string sectionAlias); - - /// - /// Add a specific section to all user groups or those specified as parameters - /// - /// This is useful when a new section is created to allow specific user groups to access it - /// Alias of the section to add - /// Specifiying nothing will add the section to all user - void AddSectionToAllUserGroups(string sectionAlias, params int[] groupIds); /// /// Get permissions set for a user and optional node ids diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 9bba28ee62..8072ecb94d 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -807,39 +807,7 @@ namespace Umbraco.Core.Services uow.Commit(); //TODO: Events? } - } - - /// - /// Add a specific section to all user groups or those specified as parameters - /// - /// This is useful when a new section is created to allow specific user groups to access it - /// Alias of the section to add - /// Specifiying nothing will add the section to all user - public void AddSectionToAllUserGroups(string sectionAlias, params int[] groupIds) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateUserGroupRepository(uow); - IEnumerable groups; - if (groupIds.Any()) - { - groups = repository.GetAll(groupIds); - } - else - { - groups = repository.GetAll(); - } - foreach (var group in groups.Where(g => g.AllowedSections.InvariantContains(sectionAlias) == false)) - { - //now add the section for each group and commit - group.AddAllowedSection(sectionAlias); - repository.AddOrUpdate(group); - } - - uow.Commit(); - //TODO: Events? - } - } + } /// /// Get permissions set for a user and node Id diff --git a/src/Umbraco.Tests/Persistence/Repositories/NotificationsRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/NotificationsRepositoryTest.cs index 79340d71b3..e9d9163b12 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/NotificationsRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/NotificationsRepositoryTest.cs @@ -45,7 +45,7 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repo = new NotificationsRepository(unitOfWork)) { - var userDto = new UserDto { ContentStartId = -1, Email = "test" , Login = "test" , MediaStartId = -1, Password = "test" , UserName = "test" , UserLanguage = "en" }; + var userDto = new UserDto {ContentStartId = -1, Email = "test", Login = "test", MediaStartId = -1, Password = "test", UserName = "test", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; unitOfWork.Database.Insert(userDto); var userNew = Mock.Of(e => e.Id == userDto.Id); @@ -81,7 +81,7 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 10; i++) { - var userDto = new UserDto { ContentStartId = -1, Email = "test" + i, Login = "test" + i, MediaStartId = -1, Password = "test", UserName = "test" + i, UserLanguage = "en" }; + var userDto = new UserDto {ContentStartId = -1, Email = "test" + i, Login = "test" + i, MediaStartId = -1, Password = "test", UserName = "test" + i, UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now}; unitOfWork.Database.Insert(userDto); var userNew = Mock.Of(e => e.Id == userDto.Id); var notification = repo.CreateNotification(userNew, (i % 2 == 0) ? entity1 : entity2, i.ToString(CultureInfo.InvariantCulture)); @@ -109,7 +109,7 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 10; i++) { - var userDto = new UserDto { ContentStartId = -1, Email = "test" + i, Login = "test" + i, MediaStartId = -1, Password = "test", UserName = "test" + i, UserLanguage = "en" }; + var userDto = new UserDto {ContentStartId = -1, Email = "test" + i, Login = "test" + i, MediaStartId = -1, Password = "test", UserName = "test" + i, UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now}; unitOfWork.Database.Insert(userDto); var userNew = Mock.Of(e => e.Id == userDto.Id); var notification = repo.CreateNotification(userNew, (i % 2 == 0) ? entity1 : entity2, i.ToString(CultureInfo.InvariantCulture)); @@ -128,7 +128,7 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repo = new NotificationsRepository(unitOfWork)) { - var userDto = new UserDto { ContentStartId = -1, Email = "test", Login = "test", MediaStartId = -1, Password = "test", UserName = "test", UserLanguage = "en" }; + var userDto = new UserDto {ContentStartId = -1, Email = "test", Login = "test", MediaStartId = -1, Password = "test", UserName = "test", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; unitOfWork.Database.Insert(userDto); var userNew = Mock.Of(e => e.Id == userDto.Id); diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 682b3695d7..d99f6e70d2 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -40,7 +40,8 @@ namespace Umbraco.Tests.Services { // Arrange var userService = ServiceContext.UserService; - var user = CreateTestUser(); + IUserGroup userGroup; + var user = CreateTestUser(out userGroup); var contentType = MockedContentTypes.CreateSimpleContentType(); ServiceContext.ContentTypeService.Save(contentType); @@ -67,7 +68,8 @@ namespace Umbraco.Tests.Services { // Arrange var userService = ServiceContext.UserService; - var user = CreateTestUser(); + IUserGroup userGroup; + var user = CreateTestUser(out userGroup); var contentType = MockedContentTypes.CreateSimpleContentType(); ServiceContext.ContentTypeService.Save(contentType); @@ -78,12 +80,12 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType) }; ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { 1 }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { 1 }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { 1 }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { 1 }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { 1 }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { 1 }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); // Act var permissions = userService.GetPermissions(user, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); @@ -476,7 +478,6 @@ namespace Umbraco.Tests.Services { var userGroup = new UserGroup { - Id = 1, Alias = "Group1", Name = "Group 1" }; @@ -521,13 +522,11 @@ namespace Umbraco.Tests.Services { var userGroup1 = new UserGroup { - Id = 1, Alias = "Group1", Name = "Group 1" }; var userGroup2 = new UserGroup { - Id = 2, Alias = "Group2", Name = "Group 2" }; @@ -555,29 +554,27 @@ namespace Umbraco.Tests.Services { var userGroup1 = new UserGroup { - Id = 1, Alias = "Group1", Name = "Group 1" }; + userGroup1.AddAllowedSection("test"); + var userGroup2 = new UserGroup { - Id = 2, Alias = "Group2", Name = "Group 2" }; + userGroup2.AddAllowedSection("test"); + var userGroup3 = new UserGroup - { - Id = 3, + { Alias = "Group3", Name = "Group 3" }; ServiceContext.UserService.Save(userGroup1); ServiceContext.UserService.Save(userGroup2); ServiceContext.UserService.Save(userGroup3); - - //now add the section to specific groups - ServiceContext.UserService.AddSectionToAllUserGroups("test", userGroup1.Id, userGroup2.Id); - + //assert var result1 = ServiceContext.UserService.GetUserGroupById(userGroup1.Id); var result2 = ServiceContext.UserService.GetUserGroupById(userGroup2.Id); @@ -587,7 +584,11 @@ namespace Umbraco.Tests.Services Assert.IsFalse(result3.AllowedSections.Contains("test")); //now add the section to all groups - ServiceContext.UserService.AddSectionToAllUserGroups("test"); + foreach (var userGroup in new[]{userGroup1, userGroup2, userGroup3}) + { + userGroup.AddAllowedSection("test"); + ServiceContext.UserService.Save(userGroup); + } //assert result1 = ServiceContext.UserService.GetUserGroupById(userGroup1.Id); @@ -668,7 +669,8 @@ namespace Umbraco.Tests.Services public void Get_User_By_Username() { // Arrange - var originalUser = CreateTestUser(); + IUserGroup userGroup; + var originalUser = CreateTestUser(out userGroup); // Act @@ -689,9 +691,9 @@ namespace Umbraco.Tests.Services Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(2)); } - private IUser CreateTestUser() + private IUser CreateTestUser(out IUserGroup userGroup) { - var userGroup = CreateTestUserGroup(); + userGroup = CreateTestUserGroup(); var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com"); user.AddGroup(userGroup.Alias); @@ -703,14 +705,16 @@ namespace Umbraco.Tests.Services { var userGroup = new UserGroup { - Id = 1, Alias = "testGroup", Name = "Test Group", Permissions = "ABCDEFGHIJ1234567".ToCharArray().Select(x => x.ToString()) }; - ServiceContext.UserService.Save(userGroup); - ServiceContext.UserService.AddSectionToAllUserGroups("content", 1); - ServiceContext.UserService.AddSectionToAllUserGroups("media", 1); + + userGroup.AddAllowedSection("content"); + userGroup.AddAllowedSection("media"); + + ServiceContext.UserService.Save(userGroup); + return userGroup; } } diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 575e354876..24d1c50a65 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -53,6 +53,15 @@ namespace Umbraco.Web.Editors return Mapper.Map(user); } + /// + /// Returns all user groups + /// + /// + public IEnumerable GetUserGroups() + { + return Mapper.Map< IEnumerable, IEnumerable>(Services.UserService.GetAllUserGroups()); + } + /// /// Returns a paged users collection /// @@ -118,12 +127,13 @@ namespace Umbraco.Web.Editors /// public bool PostDisableUsers([FromUri]int[] userIds) { - var users = Services.UserService.GetUsersById(userIds); + var users = Services.UserService.GetUsersById(userIds).ToArray(); foreach (var u in users) { - //without the permanent flag, this will just disable - Services.UserService.Delete(u); + u.IsApproved = false; } + Services.UserService.Save(users); + return true; } diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index fe1933669b..a0c2a8ef33 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -39,7 +39,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "email", IsRequired = true)] public string Email { get; set; } - + /// /// The list of group aliases assigned to the user /// @@ -60,9 +60,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "availableCultures")] public IDictionary AvailableCultures { get; set; } + //TODO: This will become StartContentIds as an array! [DataMember(Name = "startContentId")] public int StartContentId { get; set; } + //TODO: This will become StartMediaIds as an array! [DataMember(Name = "startMediaId")] public int StartMediaId { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/UserGroupDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserGroupDisplay.cs new file mode 100644 index 0000000000..06e24b7d38 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UserGroupDisplay.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "userGroup", Namespace = "")] + public class UserGroupDisplay : EntityBasic, INotificationModel + { + public UserGroupDisplay() + { + Notifications = new List(); + } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + + [DataMember(Name = "sections")] + public IEnumerable Sections { get; set; } + + [DataMember(Name = "startNodeContent")] + public int StartContentId { get; set; } + + [DataMember(Name = "startNodeMedia")] + public int StartMediaId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 00e8e9a230..885d61bccb 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -16,8 +16,9 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { - config.CreateMap() - .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) + config.CreateMap(); + + config.CreateMap() .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 43b85365d7..6666189113 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -368,6 +368,7 @@ + From 0cca342b83e67e013959e923abde6220dd20b475 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 23 May 2017 15:48:08 +1000 Subject: [PATCH 088/714] updates user groups to contain the correct db columns for start nodes, updates mappings, fixes tests. --- src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs | 8 + .../Persistence/Mappers/UserGroupMapper.cs | 4 +- .../Migrations/Initial/BaseDataCreation.cs | 8 +- .../AddUserGroupTables.cs | 9 + .../src/common/resources/users.resource.js | 297 ++++-------------- .../Models/Mapping/UserModelMapper.cs | 9 +- 6 files changed, 88 insertions(+), 247 deletions(-) diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs index 77f1fed7f1..42df2fcf1c 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -49,6 +49,14 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] public string Icon { get; set; } + [Column("startContentId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int StartContentId { get; set; } + + [Column("startMediaId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int StartMediaId { get; set; } + [ResultColumn] public List UserGroup2AppDtos { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Mappers/UserGroupMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UserGroupMapper.cs index 69386063db..cd376e79a9 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UserGroupMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UserGroupMapper.cs @@ -33,7 +33,9 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.Id, dto => dto.Id); CacheMap(src => src.Alias, dto => dto.Alias); CacheMap(src => src.Name, dto => dto.Name); - CacheMap(src => src.Permissions, dto => dto.DefaultPermissions); + CacheMap(src => src.Icon, dto => dto.Icon); + CacheMap(src => src.StartContentId, dto => dto.StartContentId); + CacheMap(src => src.StartMediaId, dto => dto.StartMediaId); } #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 434a011a28..2e0bbf9ad2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -175,10 +175,10 @@ namespace Umbraco.Core.Persistence.Migrations.Initial private void CreateUmbracoUserGroupData() { - _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 1, Alias = Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); - _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 2, Alias = "writer", Name = "Writers", DefaultPermissions = "CAH:F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); - _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 3, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); - _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 4, Alias = "translator", Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); + _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 1, Alias = Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-medal" }); + _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 2, Alias = "writer", Name = "Writers", DefaultPermissions = "CAH:F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" }); + _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 3, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" }); + _database.Insert("umbracoUserGroup", "id", false, new UserGroupDto { Id = 4, Alias = "translator", Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-globe" }); } private void CreateUmbracoUser2UserGroupData() diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index 1d2a7a3d9a..5e99a47417 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -25,9 +25,18 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe MigrateUserPermissions(); MigrateUserTypesToGroups(); DeleteOldTables(tables, constraints); + SetDefaultIcons(); } } + private void SetDefaultIcons() + { + Execute.Sql(string.Format("UPDATE umbracoUserGroup SET icon = '{0}' WHERE userGroupAlias = '{1}'", "", Constants.Security.AdminGroupAlias)); + Execute.Sql(string.Format("UPDATE umbracoUserGroup SET icon = '{0}' WHERE userGroupAlias = '{1}'", "icon-edit", "writer")); + Execute.Sql(string.Format("UPDATE umbracoUserGroup SET icon = '{0}' WHERE userGroupAlias = '{1}'", "icon-tools", "editor")); + Execute.Sql(string.Format("UPDATE umbracoUserGroup SET icon = '{0}' WHERE userGroupAlias = '{1}'", "icon-globe", "translator")); + } + private bool AddNewTables(string[] tables) { var updated = false; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index e460341c77..2c3389479f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -11,17 +11,37 @@ function usersResource($http, umbRequestHelper, $q) { - function disableUser(userId) { - if (!userId) { - throw "userId not specified"; + function disableUsers(userIds) { + if (!userIds) { + throw "userIds not specified"; } + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = "userIds=" + userIds.join("&userIds="); + + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "userApiBaseUrl", - "PostDisableUser", [{ userId: userId }])), - 'Failed to disable the user ' + userId); + "PostDisableUsers", qry)), + 'Failed to disable the users ' + userIds.join(",")); + } + + function enableUsers(userIds) { + if (!userIds) { + throw "userIds not specified"; + } + + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = "userIds=" + userIds.join("&userIds="); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostEnableUsers", qry)), + 'Failed to enable the users ' + userIds.join(",")); } function getPagedResults(options) { @@ -48,248 +68,41 @@ options.orderDirection = "Descending"; } - var params = { - pageNumber: options.pageNumber, - pageSize: options.pageSize, - orderBy: options.orderBy, - orderDirection: options.orderDirection, - filter: options.filter - }; - //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed - var qry = umbRequestHelper.dictionaryToQueryString(params); - if (options.userGroups.length > 0) { - //we need to create a custom query string for an array - qry += "&" + options.userGroups.join("&"); - } + var params = { + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + filter: options.filter + }; + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = umbRequestHelper.dictionaryToQueryString(params); + if (options.userGroups.length > 0) { + //we need to create a custom query string for an array + qry += "&userGroups=" + options.userGroups.join("&userGroups="); + } - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "GetPagedUsers", - qry)), - 'Failed to retrieve users paged result'); + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "GetPagedUsers", + qry)), + 'Failed to retrieve users paged result'); } function getUser(userId) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "GetById", - { id: userId })), - "Failed to retrieve data for user " + userId); - - } + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "GetById", + { id: userId })), + "Failed to retrieve data for user " + userId); - function getUsers() { - var deferred = $q.defer(); - var users = [ - { - "id": 1, - "name": "Tammy Contreras", - "userGroups": [ - { - "name": "Admin" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/adellecharles/128.jpg", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 2, - "name": "Edward Flores", - "userGroups": [ - { - "name": "Admin" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/marcosmoralez/128.jpg", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 3, - "name": "Benjamin Mills", - "userGroups": [ - { - "name": "Writer" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/dancounsell/128.jpg", - "state": "disabled", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 4, - "name": "Samantha Martinez", - "userGroups": [ - { - "name": "Editor" - } - ], - "avatar": "", - "state": "pending", - "lastLogin": "" - }, - { - "id": 5, - "name": "Angela Stone", - "userGroups": [ - { - "name": "Editor" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/jina/128.jpg", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 6, - "name": "Beverly Silva", - "userGroups": [ - { - "name": "Editor" - } - ], - "avatar": "", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 7, - "name": "Arthur Welch", - "userGroups": [ - { - "name": "Editor" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/ashleyford/128.jpg", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 8, - "name": "Ruth Turner", - "userGroups": [ - { - "name": "Translator" - }, - { - "name": "Editor" - } - ], - "avatar": "", - "state": "pending", - "lastLogin": "" - }, - { - "id": 9, - "name": "Tammy Contreras", - "userGroups": [ - { - "name": "Translator" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/adellecharles/128.jpg", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 10, - "name": "Edward Flores", - "userGroups": [ - { - "name": "Admin" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/marcosmoralez/128.jpg", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 11, - "name": "Benjamin Mills", - "userGroups": [ - { - "name": "Writer" - }, - { - "name": "Translator" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/dancounsell/128.jpg", - "state": "disabled", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 12, - "name": "Samantha Martinez", - "userGroupName": "Editor", - "userGroups": [ - { - "name": "Editor" - } - ], - "avatar": "", - "state": "pending", - "lastLogin": "" - }, - { - "id": 13, - "name": "Angela Stone", - "userGroups": [ - { - "name": "Editor" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/jina/128.jpg", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 14, - "name": "Beverly Silva", - "userGroups": [ - { - "name": "Editor" - } - ], - "avatar": "", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 15, - "name": "Arthur Welch", - "userGroups": [ - { - "name": "Editor" - } - ], - "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/ashleyford/128.jpg", - "state": "active", - "lastLogin": "2014-04-25T01:32:21.196Z" - }, - { - "id": 16, - "name": "Ruth Turner", - "userGroups": [ - { - "name": "Translator" - } - ], - "avatar": "", - "state": "pending", - "lastLogin": "" - } - ]; - deferred.resolve(users); - return deferred.promise; } - + function getUserRole() { var deferred = $q.defer(); var user = { @@ -302,6 +115,7 @@ return deferred.promise; } + //TODO: Change this over to the real resource function getUserGroups() { var deferred = $q.defer(); var userGroups = [ @@ -351,7 +165,8 @@ getUsers: getUsers, getUserRole: getUserRole, getUserGroups: getUserGroups, - disableUser: disableUser, + disableUsers: disableUsers, + enableUsers: enableUsers, getPagedResults: getPagedResults }; diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 885d61bccb..9d1690dd9a 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -16,7 +16,14 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { - config.CreateMap(); + config.CreateMap() + .ForMember(detail => detail.Notifications, opt => opt.Ignore()) + .ForMember(detail => detail.Sections, opt => opt.MapFrom(x => x.AllowedSections)) + .ForMember(detail => detail.Udi, opt => opt.Ignore()) + .ForMember(detail => detail.Trashed, opt => opt.Ignore()) + .ForMember(detail => detail.ParentId, opt => opt.UseValue(-1)) + .ForMember(detail => detail.Path, opt => opt.MapFrom(user => "-1," + user.Id)) + .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()); config.CreateMap() .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) From 4c852eab0bcbe831da7bfdbd43474ac4d9b5380c Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 23 May 2017 10:16:29 +0200 Subject: [PATCH 089/714] fix js error --- src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 2c3389479f..b14bb0e577 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -162,7 +162,6 @@ var resource = { getUser: getUser, - getUsers: getUsers, getUserRole: getUserRole, getUserGroups: getUserGroups, disableUsers: disableUsers, From 41460d55007d831a7935f47dc96b6fe00a94bc98 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 23 May 2017 11:02:52 +0200 Subject: [PATCH 090/714] rename from role to group --- .../src/common/resources/users.resource.js | 4 +- ...role.controller.js => group.controller.js} | 18 ++++---- .../src/views/users/{role.html => group.html} | 24 +++++----- .../src/views/users/overview.controller.js | 6 +-- .../src/views/users/user.controller.js | 24 +++++----- .../src/views/users/user.html | 28 +++++------ .../groups.controller.js} | 46 +++++++++---------- .../{roles/roles.html => groups/groups.html} | 18 ++++---- 8 files changed, 84 insertions(+), 84 deletions(-) rename src/Umbraco.Web.UI.Client/src/views/users/{role.controller.js => group.controller.js} (85%) rename src/Umbraco.Web.UI.Client/src/views/users/{role.html => group.html} (93%) rename src/Umbraco.Web.UI.Client/src/views/users/views/{roles/roles.controller.js => groups/groups.controller.js} (53%) rename src/Umbraco.Web.UI.Client/src/views/users/views/{roles/roles.html => groups/groups.html} (78%) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index c5d1f7d1db..35f85063e0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -103,7 +103,7 @@ } - function getUserRole() { + function getUserGroup() { var deferred = $q.defer(); var user = { "name": "Admin", @@ -188,7 +188,7 @@ var resource = { getUser: getUser, - getUserRole: getUserRole, + getUserGroup: getUserGroup, getUserGroups: getUserGroups, disableUsers: disableUsers, enableUsers: enableUsers, diff --git a/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js similarity index 85% rename from src/Umbraco.Web.UI.Client/src/views/users/role.controller.js rename to src/Umbraco.Web.UI.Client/src/views/users/group.controller.js index 12f23d3f7e..ef2f9d378d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/role.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js @@ -1,13 +1,13 @@ (function () { "use strict"; - function UserRoleEditController($scope, $timeout, $location, usersResource) { + function UserGroupEditController($scope, $timeout, $location, usersResource) { var vm = this; vm.loading = false; vm.page = {}; - vm.userRole = {}; + vm.userGroup = {}; vm.goToPage = goToPage; vm.openSectionPicker = openSectionPicker; @@ -21,8 +21,8 @@ vm.loading = true; // get user - usersResource.getUserRole().then(function (userRole) { - vm.userRole = userRole; + usersResource.getUserGroup().then(function (userGroup) { + vm.userGroup = userGroup; makeBreadcrumbs(); }); @@ -49,7 +49,7 @@ show: true, submit: function(model) { if(model.selection) { - vm.userRole.startNodesContent = model.selection; + vm.userGroup.startNodesContent = model.selection; } vm.contentPicker.show = false; vm.contentPicker = null; @@ -72,7 +72,7 @@ show: true, submit: function(model) { if(model.selection) { - vm.userRole.startNodesMedia = model.selection; + vm.userGroup.startNodesMedia = model.selection; } vm.contentPicker.show = false; vm.contentPicker = null; @@ -104,10 +104,10 @@ { "name": "Groups", "path": "/users/users/overview", - "subView": "roles" + "subView": "groups" }, { - "name": vm.userRole.name + "name": vm.userGroup.name } ]; } @@ -116,6 +116,6 @@ } - angular.module("umbraco").controller("Umbraco.Editors.Users.RoleController", UserRoleEditController); + angular.module("umbraco").controller("Umbraco.Editors.Users.GroupController", UserGroupEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/role.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html similarity index 93% rename from src/Umbraco.Web.UI.Client/src/views/users/role.html rename to src/Umbraco.Web.UI.Client/src/views/users/group.html index d8c79c881e..813cb80006 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/role.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -1,4 +1,4 @@ -
+
@@ -7,8 +7,8 @@ @@ -28,17 +28,17 @@ + on-remove="vm.removeSelectedItem($index, vm.userGroup.sections)"> Add @@ -47,11 +47,11 @@ + on-remove="vm.removeSelectedItem($index, vm.userGroup.startNodesContent)"> + on-remove="vm.removeSelectedItem($index, vm.userGroup.startNodesMedia)"> Users
+ on-remove="vm.removeSelectedUser($index, vm.userGroup.users)"> Permissions
- + - - + on-remove="vm.removeSelectedItem($index, vm.user.userGroups)"> + Add @@ -248,9 +248,9 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js similarity index 53% rename from src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.controller.js rename to src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index 6a602e4cfa..4c198b91a8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -1,11 +1,11 @@ (function () { "use strict"; - function UserRolesController($scope, $timeout, $location, usersResource) { + function UserGroupsController($scope, $timeout, $location, usersResource) { var vm = this; - vm.userRoles = []; + vm.userGroups = []; vm.selection = []; vm.viewState = 'overview'; @@ -15,9 +15,9 @@ }; vm.setViewState = setViewState; - vm.goToUserRole = goToUserRole; + vm.goToUserGroup = goToUserGroup; vm.clearSelection = clearSelection; - vm.selectUserRole = selectUserRole; + vm.selectUserGroup = selectUserGroup; vm.selectAll = selectAll; vm.areAllSelected = areAllSelected; @@ -26,8 +26,8 @@ vm.loading = true; // Get users - usersResource.getUserGroups().then(function (userRoles) { - vm.userRoles = userRoles; + usersResource.getUserGroups().then(function (userGroups) { + vm.userGroups = userGroups; }); // fake loading @@ -41,46 +41,46 @@ vm.viewState = state; } - function selectUserRole(userRole, selection) { - if(userRole.selected) { - var index = selection.indexOf(userRole.id); + function selectUserGroup(userGroup, selection) { + if(userGroup.selected) { + var index = selection.indexOf(userGroup.id); selection.splice(index, 1); - userRole.selected = false; + userGroup.selected = false; } else { - userRole.selected = true; - vm.selection.push(userRole.id); + userGroup.selected = true; + vm.selection.push(userGroup.id); } } function clearSelection() { - angular.forEach(vm.userRoles, function(userRole){ - userRole.selected = false; + angular.forEach(vm.userGroups, function(userGroup){ + userGroup.selected = false; }); vm.selection = []; } - function goToUserRole(userRole) { - $location.path('users/users/role/' + userRole.id); + function goToUserGroup(userGroup) { + $location.path('users/users/group/' + userGroup.id); } function selectAll() { if(areAllSelected()) { vm.selection = []; - angular.forEach(vm.userRoles, function(userRole){ - userRole.selected = false; + angular.forEach(vm.userGroups, function(userGroup){ + userGroup.selected = false; }); } else { // clear selection so we don't add the same user twice vm.selection = []; // select all users - angular.forEach(vm.userRoles, function(userRole){ - userRole.selected = true; - vm.selection.push(userRole.id); + angular.forEach(vm.userGroups, function(userGroup){ + userGroup.selected = true; + vm.selection.push(userGroup.id); }); } } function areAllSelected() { - if(vm.selection.length === vm.userRoles.length) { + if(vm.selection.length === vm.userGroups.length) { return true; } } @@ -89,6 +89,6 @@ } - angular.module("umbraco").controller("Umbraco.Editors.Users.RolesController", UserRolesController); + angular.module("umbraco").controller("Umbraco.Editors.Users.GroupsController", UserGroupsController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html similarity index 78% rename from src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.html rename to src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index b90edb55dc..c7e0c0e504 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/roles/roles.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -1,4 +1,4 @@ -
+
@@ -7,8 +7,8 @@ + action="vm.createUserGroup()" + label="Create Group"> @@ -46,7 +46,7 @@ - {{ vm.selection.length }} of {{ vm.userRoles.length }} selected + {{ vm.selection.length }} of {{ vm.userGroups.length }} selected @@ -56,16 +56,16 @@ button-style="link" label="Delete" icon="icon-trash" - action="vm.deleteUserRole()"> + action="vm.deleteUserGroup()"> -
- -
{{userRole.name}}
- Edit +
+ +
{{userGroup.name}}
+ Edit
\ No newline at end of file From f792be949560bcc6cd00605b6ed6e6eb020078cc Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 23 May 2017 15:31:51 +0200 Subject: [PATCH 091/714] wire up language selector --- src/Umbraco.Web.UI.Client/src/views/users/user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 8d16bd448e..32c4ea14c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -34,7 +34,7 @@ - From 1a637961b77bb68e9c36e2a80b0089bbe488b535 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 23 May 2017 20:57:43 +0200 Subject: [PATCH 092/714] bump avatar font size --- src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less index 85b597ab91..35409dc470 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less @@ -53,7 +53,7 @@ .umb-avatar--xxl { width: 150px; height: 150px; - font-size: 20px; + font-size: 36px; } /* Colors */ From 38837049f019ad6d8fa9c59d2cee27c0a93e8f27 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 May 2017 19:01:01 +1000 Subject: [PATCH 093/714] adds invite user endpoint with a controller test! --- .../Configuration/GlobalSettings.cs | 24 +++++++- .../Controllers/UsersControllerTests.cs | 59 ++++++++++++++++++- .../TestHelpers/BaseUmbracoApplicationTest.cs | 32 +++++----- .../TestControllerActivatorBase.cs | 4 +- .../ControllerTesting/TestRunner.cs | 7 ++- .../Editors/BackOfficeController.cs | 14 +---- src/Umbraco.Web/Editors/UsersController.cs | 41 +++++++++++++ .../Models/ContentEditing/UserInvite.cs | 26 ++++++++ .../Models/ContentEditing/UserSave.cs | 4 +- .../Models/Mapping/UserModelMapper.cs | 10 ++++ .../Identity/ExternalSignInAutoLinkOptions.cs | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 1 + 12 files changed, 186 insertions(+), 37 deletions(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/UserInvite.cs diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index acbf0065c0..02f3322ec9 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Configuration; using System.Linq; +using System.Net.Configuration; using System.Web; using System.Web.Configuration; using System.Web.Hosting; @@ -42,7 +43,6 @@ namespace Umbraco.Core.Configuration //ensure the built on (non-changeable) reserved paths are there at all times private const string StaticReservedPaths = "~/app_plugins/,~/install/,"; private const string StaticReservedUrls = "~/config/splashes/booting.aspx,~/config/splashes/noNodes.aspx,~/VSEnterpriseHelper.axd,"; - #endregion /// @@ -53,6 +53,7 @@ namespace Umbraco.Core.Configuration _reservedUrlsCache = null; _reservedPaths = null; _reservedUrls = null; + HasSmtpServer = null; } /// @@ -64,7 +65,26 @@ namespace Umbraco.Core.Configuration ResetInternal(); } - /// + public static bool HasSmtpServerConfigured(string appPath) + { + if (HasSmtpServer.HasValue) return HasSmtpServer.Value; + + var config = WebConfigurationManager.OpenWebConfiguration(appPath); + var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings"); + if (settings == null || settings.Smtp == null) return false; + if (settings.Smtp.SpecifiedPickupDirectory != null && string.IsNullOrEmpty(settings.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation) == false) + return true; + if (settings.Smtp.Network != null && string.IsNullOrEmpty(settings.Smtp.Network.Host) == false) + return true; + return false; + } + + /// + /// For testing only + /// + internal static bool? HasSmtpServer { get; set; } + + /// /// Gets the reserved urls from web.config. /// /// The reserved urls. diff --git a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Controllers/UsersControllerTests.cs index 45a4979377..b722bc789b 100644 --- a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Controllers/UsersControllerTests.cs @@ -32,14 +32,66 @@ using Umbraco.Web.WebApi; namespace Umbraco.Tests.Controllers { [DatabaseTestBehavior(DatabaseBehavior.NoDatabasePerFixture)] - [RequiresAutoMapperMappings] [TestFixture] public class UsersControllerTests : BaseDatabaseFactoryTest { + + [Test] + public async void Invite_User() + { + var runner = new TestRunner((message, helper) => + { + //setup some mocks + Umbraco.Core.Configuration.GlobalSettings.HasSmtpServer = true; + + var userServiceMock = Mock.Get(helper.UmbracoContext.Application.Services.UserService); + + userServiceMock.Setup(service => service.Save(It.IsAny(), It.IsAny())) + .Callback((IUser u, bool raiseEvents) => + { + u.Id = 1234; + }); + userServiceMock.Setup(service => service.GetAllUserGroups(It.IsAny())) + .Returns(Enumerable.Empty); + + //we need to manually apply automapper mappings with the mocked applicationcontext + InitializeMappers(helper.UmbracoContext.Application); + + return new UsersController(helper.UmbracoContext); + }); + + var invite = new UserInvite + { + Id = -1, + Email = "test@test.com", + Message = "Hello test!", + Name = "Test", + UserGroups = new[] {"writers"} + }; + var response = await runner.Execute("Users", "PostInviteUser", HttpMethod.Post, + new ObjectContent(invite, new JsonMediaTypeFormatter())); + + var obj = JsonConvert.DeserializeObject(response.Item2); + + Assert.AreEqual(invite.Name, obj.Name); + Assert.AreEqual(1234, obj.Id); + Assert.AreEqual(invite.Email, obj.Email); + foreach (var group in invite.UserGroups) + { + Assert.IsTrue(obj.UserGroups.Contains(group)); + } + } + [Test] public async void GetPagedUsers_Empty() { - var runner = new TestRunner((message, helper) => new UsersController(helper.UmbracoContext)); + var runner = new TestRunner((message, helper) => + { + //we need to manually apply automapper mappings with the mocked applicationcontext + InitializeMappers(helper.UmbracoContext.Application); + + return new UsersController(helper.UmbracoContext); + }); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); var obj = JsonConvert.DeserializeObject>(response.Item2); @@ -58,6 +110,9 @@ namespace Umbraco.Tests.Controllers userServiceMock.Setup(service => service.GetAll(It.IsAny(), It.IsAny(), out outVal, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => users); + //we need to manually apply automapper mappings with the mocked applicationcontext + InitializeMappers(helper.UmbracoContext.Application); + return new UsersController(helper.UmbracoContext); }); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 87af8f3234..fa4e851b5e 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -52,7 +52,10 @@ namespace Umbraco.Tests.TestHelpers SetupApplicationContext(); - InitializeMappers(); + if (GetType().GetCustomAttribute(false) != null) + { + InitializeMappers(ApplicationContext); + } FreezeResolution(); @@ -98,24 +101,21 @@ namespace Umbraco.Tests.TestHelpers /// This is an opt-in option because initializing the mappers takes about 500ms which equates to quite a lot /// of time with every test. /// - private void InitializeMappers() + protected virtual void InitializeMappers(ApplicationContext applicationContext) { - if (GetType().GetCustomAttribute(false) != null) + Mapper.Initialize(configuration => { - Mapper.Initialize(configuration => - { - var mappers = PluginManager.Current.FindAndCreateInstances( - specificAssemblies: new[] - { - typeof(ContentModelMapper).Assembly, - typeof(ApplicationRegistrar).Assembly - }); - foreach (var mapper in mappers) + var mappers = PluginManager.Current.FindAndCreateInstances( + specificAssemblies: new[] { - mapper.ConfigureMappings(configuration, ApplicationContext); - } - }); - } + typeof(ContentModelMapper).Assembly, + typeof(ApplicationRegistrar).Assembly + }); + foreach (var mapper in mappers) + { + mapper.ConfigureMappings(configuration, applicationContext); + } + }); } /// diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index bebf6ab1e2..7299d9758e 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -49,7 +49,9 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting var serviceContext = new ServiceContext( userService: mockedUserService, - migrationEntryService: mockedMigrationService.Object); + migrationEntryService: mockedMigrationService.Object, + localizedTextService:Mock.Of(), + sectionService:Mock.Of()); //ensure the configuration matches the current version for tests SettingsForTests.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToSemanticString(); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs index 32e3a08e31..cc024b40a2 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs @@ -22,7 +22,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting _controllerFactory = controllerFactory; } - public async Task> Execute(string controllerName, string actionName, HttpMethod method) + public async Task> Execute(string controllerName, string actionName, HttpMethod method, HttpContent content = null) { var startup = new TestStartup( configuration => @@ -38,9 +38,12 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting var request = new HttpRequestMessage { RequestUri = new Uri("https://testserver/"), - Method = method, + Method = method }; + if (content != null) + request.Content = content; + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); Console.WriteLine(request); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index fd70d71c99..a4260b7a49 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -401,7 +401,7 @@ namespace Umbraco.Web.Editors {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')}, {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset}, {"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage}, - {"emailServerConfigured", HasSmtpServerConfigured()}, + {"emailServerConfigured", GlobalSettings.HasSmtpServerConfigured(HttpContext.Request.ApplicationPath)}, } }, { @@ -449,17 +449,7 @@ namespace Umbraco.Web.Editors return JavaScript(result); } - private bool HasSmtpServerConfigured() - { - var config = WebConfigurationManager.OpenWebConfiguration(HttpContext.Request.ApplicationPath); - var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings"); - if (settings == null || settings.Smtp == null) return false; - if (settings.Smtp.SpecifiedPickupDirectory != null && string.IsNullOrEmpty(settings.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation) == false) - return true; - if (settings.Smtp.Network != null && string.IsNullOrEmpty(settings.Smtp.Network.Host) == false) - return true; - return false; - } + [HttpPost] public ActionResult ExternalLogin(string provider, string redirectUrl = null) diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 24d1c50a65..c6ce2ecc81 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -3,15 +3,18 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Web; using System.Web.Http; using AutoMapper; using ClientDependency.Core; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -95,6 +98,44 @@ namespace Umbraco.Web.Editors }; } + /// + /// Invites a user + /// + /// + /// + /// + /// This will email the user an invite and generate a token that will be validated in the email + /// + public UserDisplay PostInviteUser(UserInvite userSave) + { + if (userSave == null) throw new ArgumentNullException("userSave"); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var hasSmtp = GlobalSettings.HasSmtpServerConfigured(RequestContext.VirtualPathRoot); + if (hasSmtp == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse("No Email server is configured")); + } + + var existing = Services.UserService.GetByEmail(userSave.Email); + if (existing != null) + { + ModelState.AddModelError("Email", "A user with the email already exists"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var user = Mapper.Map(userSave); + + Services.UserService.Save(user); + + return Mapper.Map(user); + } + /// /// Saves a user /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs b/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs new file mode 100644 index 0000000000..f3bcb80005 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Represents the data used to invite a user + /// + [DataContract(Name = "user", Namespace = "")] + public class UserInvite : EntityBasic + { + [DataMember(Name = "userGroups")] + [Required] + public IEnumerable UserGroups { get; set; } + + [DataMember(Name = "email", IsRequired = true)] + [Required] + [EmailAddress] + public string Email { get; set; } + + [DataMember(Name = "message", IsRequired = true)] + [Required] + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs index 5fe57e0787..8bef19d62f 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs @@ -26,9 +26,9 @@ namespace Umbraco.Web.Models.ContentEditing [EmailAddress] public string Email { get; set; } - [DataMember(Name = "userType")] + [DataMember(Name = "userGroups")] [Required] - public string UserType { get; set; } + public IEnumerable UserGroups { get; set; } [DataMember(Name = "startContentId")] public int StartContentId { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 9d1690dd9a..aa4ad4b6e4 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -16,6 +16,16 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + config.CreateMap() + .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) + .AfterMap((invite, user) => + { + foreach (var group in invite.UserGroups) + { + user.AddGroup(group); + } + }); + config.CreateMap() .ForMember(detail => detail.Notifications, opt => opt.Ignore()) .ForMember(detail => detail.Sections, opt => opt.MapFrom(x => x.AllowedSections)) diff --git a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs index 832f0b3a30..5b637de3b8 100644 --- a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs @@ -26,6 +26,7 @@ namespace Umbraco.Web.Security.Identity _defaultCulture = defaultCulture ?? GlobalSettings.DefaultUILanguage; } + //TODO: Change this - it will be different when we have user groups! private readonly string _defaultUserType; /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 6666189113..6fdc3692fc 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -369,6 +369,7 @@ + From dec9422531927f44e34642b018c07b52fd54d290 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 25 May 2017 02:03:41 +1000 Subject: [PATCH 094/714] Updated DB and business logic to support multiple starts nodes for a user and for user groups to have start nodes - U4-9915 User Groups will have a start node assigned and Users can have multiple start nodes assigned --- src/Umbraco.Core/Models/ContentType.cs | 10 +-- src/Umbraco.Core/Models/ContentTypeBase.cs | 15 +++-- src/Umbraco.Core/Models/DictionaryItem.cs | 11 ++-- .../Models/Identity/BackOfficeIdentityUser.cs | 8 +-- .../Models/Identity/IdentityModelMappings.cs | 8 +-- .../Models/Membership/IProfile.cs | 12 ++-- src/Umbraco.Core/Models/Membership/IUser.cs | 4 +- src/Umbraco.Core/Models/Membership/User.cs | 61 +++++++++++-------- .../Models/Membership/UserGroup.cs | 11 ++-- .../Models/Membership/UserProfile.cs | 46 ++++++++++++++ src/Umbraco.Core/Models/Property.cs | 50 +++++++-------- src/Umbraco.Core/Models/Rdbms/UserDto.cs | 13 ++-- .../Models/Rdbms/UserStartNodeDto.cs | 36 +++++++++++ src/Umbraco.Core/Models/UserExtensions.cs | 22 +++++-- .../Persistence/Factories/UserFactory.cs | 29 +++++++-- .../Persistence/Mappers/UserMapper.cs | 4 +- .../Migrations/Initial/BaseDataCreation.cs | 3 +- .../Initial/DatabaseSchemaCreation.cs | 1 + .../Initial/DatabaseSchemaResult.cs | 2 +- .../AddUserGroupTables.cs | 8 ++- .../AddUserStartNodeTable.cs | 49 +++++++++++++++ .../Persistence/Relators/UserGroupRelator.cs | 49 ++++++++++----- .../Interfaces/IUserRepository.cs | 3 + .../Repositories/UserRepository.cs | 57 +++++++++++++---- .../BackOfficeClaimsIdentityFactory.cs | 4 +- .../Security/BackOfficeUserStore.cs | 12 ++-- .../Security/UmbracoBackOfficeIdentity.cs | 32 ++++++---- src/Umbraco.Core/Security/UserData.cs | 4 +- src/Umbraco.Core/Services/UserService.cs | 18 +++--- src/Umbraco.Core/Umbraco.Core.csproj | 3 + .../Controllers/UsersControllerTests.cs | 46 ++++++++++++++ .../Models/UserExtensionsTests.cs | 2 +- src/Umbraco.Tests/Models/UserTests.cs | 8 +-- .../NotificationsRepositoryTest.cs | 8 +-- .../Repositories/UserRepositoryTest.cs | 12 ++-- .../UmbracoBackOfficeIdentityTests.cs | 16 ++--- .../Services/UserServiceTests.cs | 5 +- .../AuthenticateEverythingMiddleware.cs | 4 +- .../TestControllerActivatorBase.cs | 4 +- .../TestHelpers/Entities/MockedUser.cs | 2 - .../UmbracoExamine/IndexInitializer.cs | 2 +- .../ContentControllerUnitTests.cs | 26 ++++---- ...terAllowedOutgoingContentAttributeTests.cs | 4 +- .../WebApiEditors/MediaControllerUnitTests.cs | 14 ++--- .../components/tree/umbtree.directive.js | 51 +++++++++------- .../src/common/services/user.service.js | 10 --- .../common/dialogs/linkpicker.controller.js | 18 +++--- .../linkpicker/linkpicker.controller.js | 30 ++++----- .../dashboard/dashboard.tabs.controller.js | 4 +- .../grid/editors/media.controller.js | 2 +- .../grid/editors/rte.controller.js | 2 +- .../mediapicker/mediapicker.controller.js | 2 +- .../propertyeditors/rte/rte.controller.js | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 20 +++--- src/Umbraco.Web/Editors/EntityController.cs | 60 ++++++++++-------- src/Umbraco.Web/Editors/MediaController.cs | 20 +++--- src/Umbraco.Web/Editors/UsersController.cs | 53 +++++++++++++++- .../Models/ContentEditing/UserDetail.cs | 8 +-- .../Models/ContentEditing/UserDisplay.cs | 30 +++------ .../Models/ContentEditing/UserGroupDisplay.cs | 9 ++- .../Models/ContentEditing/UserSave.cs | 8 +-- .../Models/Mapping/UserModelMapper.cs | 47 +++++++++----- src/Umbraco.Web/Security/WebSecurity.cs | 2 - .../Trees/ContentTreeController.cs | 8 +-- .../Trees/ContentTreeControllerBase.cs | 20 +++--- src/Umbraco.Web/Trees/MediaTreeController.cs | 8 +-- .../FilterAllowedOutgoingContentAttribute.cs | 4 +- .../FilterAllowedOutgoingMediaAttribute.cs | 8 +-- .../BasePages/BasePage.cs | 4 +- src/umbraco.businesslogic/User.cs | 27 ++++---- 70 files changed, 774 insertions(+), 421 deletions(-) create mode 100644 src/Umbraco.Core/Models/Membership/UserProfile.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserStartNodeTable.cs diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 88c498a147..81b5272a58 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -54,6 +54,11 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo DefaultTemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.DefaultTemplateId); public readonly PropertyInfo AllowedTemplatesSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedTemplates); + + //Custom comparer for enumerable + public readonly DelegateEqualityComparer> TemplateComparer = new DelegateEqualityComparer>( + (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), + templates => templates.GetHashCode()); } /// @@ -91,10 +96,7 @@ namespace Umbraco.Core.Models set { SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, Ps.Value.AllowedTemplatesSelector, - //Custom comparer for enumerable - new DelegateEqualityComparer>( - (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), - templates => templates.GetHashCode())); + Ps.Value.TemplateComparer); } } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 88476f946d..db0bc0e900 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -88,6 +88,12 @@ namespace Umbraco.Core.Models public readonly PropertyInfo PropertyGroupCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups); public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes); public readonly PropertyInfo HasPropertyTypeBeenRemovedSelector = ExpressionHelper.GetPropertyInfo(x => x.HasPropertyTypeBeenRemoved); + + //Custom comparer for enumerable + public readonly DelegateEqualityComparer> ContentTypeSortComparer = + new DelegateEqualityComparer>( + (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable), + sorts => sorts.GetHashCode()); } @@ -254,7 +260,7 @@ namespace Umbraco.Core.Models set { SetPropertyValueAndDetectChanges(value, ref _trashed, Ps.Value.TrashedSelector); } } - private IDictionary _additionalData; + private readonly IDictionary _additionalData; /// /// Some entities may expose additional data that other's might not, this custom data will be available in this collection /// @@ -273,11 +279,8 @@ namespace Umbraco.Core.Models get { return _allowedContentTypes; } set { - SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, Ps.Value.AllowedContentTypesSelector, - //Custom comparer for enumerable - new DelegateEqualityComparer>( - (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable), - sorts => sorts.GetHashCode())); + SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, Ps.Value.AllowedContentTypesSelector, + Ps.Value.ContentTypeSortComparer); } } diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index 42b047e35b..d5fcc89994 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -37,6 +37,12 @@ namespace Umbraco.Core.Models public readonly PropertyInfo ParentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ParentId); public readonly PropertyInfo ItemKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ItemKey); public readonly PropertyInfo TranslationsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Translations); + + //Custom comparer for enumerable + public readonly DelegateEqualityComparer> DictionaryTranslationComparer = + new DelegateEqualityComparer>( + (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), + enumerable => enumerable.GetHashCode()); } /// @@ -79,10 +85,7 @@ namespace Umbraco.Core.Models } SetPropertyValueAndDetectChanges(asArray, ref _translations, Ps.Value.TranslationsSelector, - //Custom comparer for enumerable - new DelegateEqualityComparer>( - (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), - enumerable => enumerable.GetHashCode())); + Ps.Value.DictionaryTranslationComparer); } } } diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 03631bda05..52f56fc2d5 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -14,8 +14,8 @@ namespace Umbraco.Core.Models.Identity public BackOfficeIdentityUser() { - StartMediaId = -1; - StartContentId = -1; + StartMediaIds = new int[] { }; + StartContentIds = new int[] { }; Culture = Configuration.GlobalSettings.DefaultUILanguage; } @@ -31,8 +31,8 @@ namespace Umbraco.Core.Models.Identity /// Gets/sets the user's real name /// public string Name { get; set; } - public int StartContentId { get; set; } - public int StartMediaId { get; set; } + public int[] StartContentIds { get; set; } + public int[] StartMediaIds { get; set; } public string[] AllowedSections { get; set; } public string[] Groups { get; set; } public string Culture { get; set; } diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 8295ae566d..9d01056f48 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -21,8 +21,8 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.PasswordHash, expression => expression.MapFrom(user => GetPasswordHash(user.RawPasswordValue))) .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember(user => user.Name, expression => expression.MapFrom(user => user.Name)) - .ForMember(user => user.StartMediaId, expression => expression.MapFrom(user => user.StartMediaId)) - .ForMember(user => user.StartContentId, expression => expression.MapFrom(user => user.StartContentId)) + .ForMember(user => user.StartMediaIds, expression => expression.MapFrom(user => user.StartMediaIds)) + .ForMember(user => user.StartContentIds, expression => expression.MapFrom(user => user.StartContentIds)) .ForMember(user => user.AccessFailedCount, expression => expression.MapFrom(user => user.FailedPasswordAttempts)) .ForMember(user => user.Groups, expression => expression.MapFrom(user => user.Groups.ToArray())) .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); @@ -33,8 +33,8 @@ namespace Umbraco.Core.Models.Identity .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Groups)) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) - .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) - .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.StartContentNodes, opt => opt.MapFrom(user => user.StartContentIds)) + .ForMember(detail => detail.StartMediaNodes, opt => opt.MapFrom(user => user.StartMediaIds)) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.UserName)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture)) .ForMember(detail => detail.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp)); diff --git a/src/Umbraco.Core/Models/Membership/IProfile.cs b/src/Umbraco.Core/Models/Membership/IProfile.cs index 749c3371b8..0e6267a10b 100644 --- a/src/Umbraco.Core/Models/Membership/IProfile.cs +++ b/src/Umbraco.Core/Models/Membership/IProfile.cs @@ -1,15 +1,11 @@ namespace Umbraco.Core.Models.Membership { /// - /// Defines the the Profile interface - /// - /// - /// This interface is pretty useless but has been exposed publicly from 6.x so we're stuck with it. It would make more sense - /// if the Id was an int but since it's not people have to cast it to int all of the time! - /// + /// Defines the the User Profile interface + /// public interface IProfile { - object Id { get; set; } - string Name { get; set; } + int Id { get; } + string Name { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index edb9d9aa7d..0b17fea05b 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -13,8 +13,8 @@ namespace Umbraco.Core.Models.Membership string Name { get; set; } int SessionTimeout { get; set; } - int StartContentId { get; set; } - int StartMediaId { get; set; } + int[] StartContentIds { get; set; } + int[] StartMediaIds { get; set; } string Language { get; set; } /// diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 055b3e439d..f06c373360 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -27,8 +27,8 @@ namespace Umbraco.Core.Models.Membership _language = GlobalSettings.DefaultUILanguage; _isApproved = true; _isLockedOut = false; - _startContentId = -1; - _startMediaId = -1; + _startContentIds = new int[] { }; + _startMediaIds = new int[] { }; //cannot be null _rawPasswordValue = ""; _allowedSections = new List(); @@ -57,8 +57,8 @@ namespace Umbraco.Core.Models.Membership _userGroups = new List(); _isApproved = true; _isLockedOut = false; - _startContentId = -1; - _startMediaId = -1; + _startContentIds = new int[] { }; + _startMediaIds = new int[] { }; } /// @@ -88,15 +88,15 @@ namespace Umbraco.Core.Models.Membership _userGroups = new List(userGroups); _isApproved = true; _isLockedOut = false; - _startContentId = -1; - _startMediaId = -1; + _startContentIds = new int[] { }; + _startMediaIds = new int[] { }; } private string _name; private string _securityStamp; private int _sessionTimeout; - private int _startContentId; - private int _startMediaId; + private int[] _startContentIds; + private int[] _startMediaIds; private int _failedLoginAttempts; private string _username; @@ -124,8 +124,8 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo SecurityStampSelector = ExpressionHelper.GetPropertyInfo(x => x.SecurityStamp); public readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); - public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentId); - public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); + public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentIds); + public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaIds); public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo UsernameSelector = ExpressionHelper.GetPropertyInfo(x => x.Username); @@ -138,6 +138,12 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo DefaultToLiveEditingSelector = ExpressionHelper.GetPropertyInfo(x => x.DefaultToLiveEditing); public readonly PropertyInfo UserGroupsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Groups); + + //Custom comparer for enumerable + public readonly DelegateEqualityComparer> IntegerEnumerableComparer = + new DelegateEqualityComparer>( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); } #region Implementation of IMembershipUser @@ -252,7 +258,7 @@ namespace Umbraco.Core.Models.Membership public IProfile ProfileData { - get { return new UserProfile(this); } + get { return new WrappedUserProfile(this); } } /// @@ -285,10 +291,11 @@ namespace Umbraco.Core.Models.Membership /// The start content id. /// [DataMember] - public int StartContentId + [DoNotClone] + public int[] StartContentIds { - get { return _startContentId; } - set { SetPropertyValueAndDetectChanges(value, ref _startContentId, Ps.Value.StartContentIdSelector); } + get { return _startContentIds; } + set { SetPropertyValueAndDetectChanges(value, ref _startContentIds, Ps.Value.StartContentIdSelector, Ps.Value.IntegerEnumerableComparer); } } /// @@ -297,11 +304,12 @@ namespace Umbraco.Core.Models.Membership /// /// The start media id. /// - [DataMember] - public int StartMediaId + [DataMember] + [DoNotClone] + public int[] StartMediaIds { - get { return _startMediaId; } - set { SetPropertyValueAndDetectChanges(value, ref _startMediaId, Ps.Value.StartMediaIdSelector); } + get { return _startMediaIds; } + set { SetPropertyValueAndDetectChanges(value, ref _startMediaIds, Ps.Value.StartMediaIdSelector, Ps.Value.IntegerEnumerableComparer); } } [DataMember] @@ -349,7 +357,10 @@ namespace Umbraco.Core.Models.Membership public override object DeepClone() { - var clone = (User)base.DeepClone(); + var clone = (User)base.DeepClone(); + //manually clone the start node props + clone._startContentIds = _startContentIds.ToArray(); + clone._startMediaIds = _startMediaIds.ToArray(); //turn off change tracking clone.DisableChangeTracking(); //need to create new collections otherwise they'll get copied by ref @@ -367,28 +378,26 @@ namespace Umbraco.Core.Models.Membership /// /// Internal class used to wrap the user in a profile /// - private class UserProfile : IProfile + private class WrappedUserProfile : IProfile { private readonly IUser _user; - public UserProfile(IUser user) + public WrappedUserProfile(IUser user) { _user = user; } - public object Id + public int Id { get { return _user.Id; } - set { _user.Id = (int)value; } } public string Name { get { return _user.Name; } - set { _user.Name = value; } } - protected bool Equals(UserProfile other) + private bool Equals(WrappedUserProfile other) { return _user.Equals(other._user); } @@ -398,7 +407,7 @@ namespace Umbraco.Core.Models.Membership if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((UserProfile) obj); + return Equals((WrappedUserProfile) obj); } public override int GetHashCode() diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index b008f4652f..d6674b579d 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -32,6 +32,12 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo IconSelector = ExpressionHelper.GetPropertyInfo(x => x.Icon); public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentId); public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); + + //Custom comparer for enumerable + public readonly DelegateEqualityComparer> StringEnumerableComparer = + new DelegateEqualityComparer>( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); } public UserGroup() @@ -93,10 +99,7 @@ namespace Umbraco.Core.Models.Membership set { SetPropertyValueAndDetectChanges(value, ref _permissions, Ps.Value.PermissionsSelector, - //Custom comparer for enumerable - new DelegateEqualityComparer>( - (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), - enum1 => enum1.GetHashCode())); + Ps.Value.StringEnumerableComparer); } } diff --git a/src/Umbraco.Core/Models/Membership/UserProfile.cs b/src/Umbraco.Core/Models/Membership/UserProfile.cs new file mode 100644 index 0000000000..d32854ab1b --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserProfile.cs @@ -0,0 +1,46 @@ +using System; + +namespace Umbraco.Core.Models.Membership +{ + internal class UserProfile : IProfile, IEquatable + { + public UserProfile(int id, string name) + { + Id = id; + Name = name; + } + + public int Id { get; private set; } + public string Name { get; private set; } + + public bool Equals(UserProfile other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id == other.Id; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((UserProfile) obj); + } + + public override int GetHashCode() + { + return Id; + } + + public static bool operator ==(UserProfile left, UserProfile right) + { + return Equals(left, right); + } + + public static bool operator !=(UserProfile left, UserProfile right) + { + return Equals(left, right) == false; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index dff6a712c7..5df2eaae90 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -49,36 +49,36 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo ValueSelector = ExpressionHelper.GetPropertyInfo(x => x.Value); public readonly PropertyInfo VersionSelector = ExpressionHelper.GetPropertyInfo(x => x.Version); - } - private static readonly DelegateEqualityComparer ValueComparer = new DelegateEqualityComparer( - (o, o1) => - { - if (o == null && o1 == null) return true; - - //custom comparer for strings. - if (o is string || o1 is string) + public readonly DelegateEqualityComparer PropertyValueComparer = new DelegateEqualityComparer( + (o, o1) => { - //if one is null and another is empty then they are the same - if ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) + if (o == null && o1 == null) return true; + + //custom comparer for strings. + if (o is string || o1 is string) { - return true; + //if one is null and another is empty then they are the same + if ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) + { + return true; + } + if (o == null || o1 == null) return false; + return o.Equals(o1); } + if (o == null || o1 == null) return false; + + //Custom comparer for enumerable if it is enumerable + var enum1 = o as IEnumerable; + var enum2 = o1 as IEnumerable; + if (enum1 != null && enum2 != null) + { + return enum1.Cast().UnsortedSequenceEqual(enum2.Cast()); + } return o.Equals(o1); - } - - if (o == null || o1 == null) return false; - - //Custom comparer for enumerable if it is enumerable - var enum1 = o as IEnumerable; - var enum2 = o1 as IEnumerable; - if (enum1 != null && enum2 != null) - { - return enum1.Cast().UnsortedSequenceEqual(enum2.Cast()); - } - return o.Equals(o1); - }, o => o.GetHashCode()); + }, o => o.GetHashCode()); + } /// /// Returns the instance of the tag support, by default tags are not enabled @@ -200,7 +200,7 @@ namespace Umbraco.Core.Models } } - SetPropertyValueAndDetectChanges(value, ref _value, Ps.Value.ValueSelector, ValueComparer); + SetPropertyValueAndDetectChanges(value, ref _value, Ps.Value.ValueSelector, Ps.Value.PropertyValueComparer); } } diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 5d2257d01b..0f74a92b7b 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -14,6 +14,7 @@ namespace Umbraco.Core.Models.Rdbms public UserDto() { UserGroupDtos = new List(); + UserStartNodeDtos = new List(); } [Column("id")] @@ -27,14 +28,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("userNoConsole")] [Constraint(Default = "0")] public bool NoConsole { get; set; } - - [Column("startStructureID")] - public int ContentStartId { get; set; } - - [Column("startMediaID")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? MediaStartId { get; set; } - + [Column("userName")] public string UserName { get; set; } @@ -88,5 +82,8 @@ namespace Umbraco.Core.Models.Rdbms [ResultColumn] public List UserGroupDtos { get; set; } + + [ResultColumn] + public List UserStartNodeDtos { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs new file mode 100644 index 0000000000..e872111829 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs @@ -0,0 +1,36 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUserStartNode")] + [PrimaryKey("id", autoIncrement = true)] + [ExplicitColumns] + internal class UserStartNodeDto + { + [Column("id")] + [PrimaryKeyColumn(Name = "PK_userStartNode")] + public int Id { get; set; } + + [Column("userId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("startNode")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(NodeDto))] + public int StartNode { get; set; } + + [Column("startNodeType")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode", Name = "IX_umbracoUserStartNode_startNodeType")] + public int StartNodeType { get; set; } + + public enum StartNodeTypeValue + { + Content = 1, + Media = 2 + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 87b97df602..4894be7b87 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -49,24 +49,34 @@ namespace Umbraco.Core.Models { if (user == null) throw new ArgumentNullException("user"); if (content == null) throw new ArgumentNullException("content"); - return HasPathAccess(content.Path, user.StartContentId, Constants.System.RecycleBinContent); + return HasPathAccess(content.Path, user.StartContentIds, Constants.System.RecycleBinContent); } - internal static bool HasPathAccess(string path, int startNodeId, int recycleBinId) + internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) { Mandate.ParameterNotNullOrEmpty(path, "path"); var formattedPath = "," + path + ","; - var formattedStartNodeId = "," + startNodeId.ToInvariantString() + ","; var formattedRecycleBinId = "," + recycleBinId.ToInvariantString() + ","; //only users with root access have access to the recycle bin if (formattedPath.Contains(formattedRecycleBinId)) { - return startNodeId == Constants.System.Root; + var hasAccess = startNodeIds.Length == 0 || startNodeIds.Contains(Constants.System.Root); + return hasAccess; + } + + //check for normal paths + foreach (var startNodeId in startNodeIds) + { + var formattedStartNodeId = "," + startNodeId.ToInvariantString() + ","; + + var hasAccess = formattedPath.Contains(formattedStartNodeId); + if (hasAccess) + return true; } - return formattedPath.Contains(formattedStartNodeId); + return false; } /// @@ -79,7 +89,7 @@ namespace Umbraco.Core.Models { if (user == null) throw new ArgumentNullException("user"); if (media == null) throw new ArgumentNullException("media"); - return HasPathAccess(media.Path, user.StartMediaId, Constants.System.RecycleBinMedia); + return HasPathAccess(media.Path, user.StartMediaIds, Constants.System.RecycleBinMedia); } /// diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 81a4b88289..e7f2aba08b 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; @@ -31,8 +32,8 @@ namespace Umbraco.Core.Persistence.Factories user.DisableChangeTracking(); user.Key = guidId; - user.StartContentId = dto.ContentStartId; - user.StartMediaId = dto.MediaStartId ?? -1; + user.StartContentIds = dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.Id).ToArray(); + user.StartMediaIds = dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int) UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.Id).ToArray(); user.IsLockedOut = dto.NoConsole; user.IsApproved = dto.Disabled == false; user.Language = dto.UserLanguage; @@ -59,9 +60,7 @@ namespace Umbraco.Core.Persistence.Factories public static UserDto BuildDto(IUser entity) { var dto = new UserDto - { - ContentStartId = entity.StartContentId, - MediaStartId = entity.StartMediaId, + { Disabled = entity.IsApproved == false, Email = entity.Email, Login = entity.Username, @@ -78,6 +77,26 @@ namespace Umbraco.Core.Persistence.Factories UpdateDate = entity.UpdateDate }; + foreach (var startNodeId in entity.StartContentIds) + { + dto.UserStartNodeDtos.Add(new UserStartNodeDto + { + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Content, + UserId = entity.Id + }); + } + + foreach (var startNodeId in entity.StartMediaIds) + { + dto.UserStartNodeDtos.Add(new UserStartNodeDto + { + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Media, + UserId = entity.Id + }); + } + if (entity.HasIdentity) { dto.Id = entity.Id.SafeCast(); diff --git a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs index 9ea69abbf6..06e906143a 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs @@ -34,9 +34,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.RawPasswordValue, dto => dto.Password); CacheMap(src => src.Name, dto => dto.UserName); //NOTE: This column in the db is *not* used! - //CacheMap(src => src.DefaultPermissions, dto => dto.DefaultPermissions); - CacheMap(src => src.StartMediaId, dto => dto.MediaStartId); - CacheMap(src => src.StartContentId, dto => dto.ContentStartId); + //CacheMap(src => src.DefaultPermissions, dto => dto.DefaultPermissions); CacheMap(src => src.IsApproved, dto => dto.Disabled); CacheMap(src => src.IsLockedOut, dto => dto.NoConsole); CacheMap(src => src.Language, dto => dto.UserLanguage); diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 2e0bbf9ad2..456859a5ea 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -169,8 +169,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial private void CreateUmbracoUserData() { - _database.Insert("umbracoUser", "id", false, new UserDto { Id = 0, Disabled = false, NoConsole = false, ContentStartId = -1, MediaStartId = -1, UserName = "Administrator", Login = "admin", Password = "default", Email = "", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); - //_database.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); + _database.Insert("umbracoUser", "id", false, new UserDto { Id = 0, Disabled = false, NoConsole = false, UserName = "Administrator", Login = "admin", Password = "default", Email = "", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); } private void CreateUmbracoUserGroupData() diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 61a6e66bb4..d23b23b4b6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -90,6 +90,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {51, typeof (User2UserGroupDto) }, {52, typeof (UserGroup2NodePermissionDto) }, {53, typeof (UserGroup2AppDto) }, + {54, typeof (UserStartNodeDto) }, }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs index 45438da9d8..c11c413f56 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs @@ -143,7 +143,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial } //if the error is for umbracoUserGroup it must be the previous version to 7.7 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoUserGroup")))) + if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoUserStartNode")))) { return new Version(7, 6, 0); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index 5e99a47417..af9805221d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -3,6 +3,7 @@ using System.Data; using System.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.SqlSyntax; @@ -63,8 +64,11 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe Create.Table(); updated = true; } + + + return updated; - } + } private void MigrateUserTypesToGroups() { @@ -158,7 +162,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe if (tables.InvariantContains("umbracoUserType") && tables.InvariantContains("umbracoUser")) { if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_umbracoUser_umbracoUserType_id"))) - { + { Delete.ForeignKey("FK_umbracoUser_umbracoUserType_id").OnTable("umbracoUser"); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserStartNodeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserStartNodeTable.cs new file mode 100644 index 0000000000..086949663b --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserStartNodeTable.cs @@ -0,0 +1,49 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero +{ + [Migration("7.7.0", 2, Constants.System.UmbracoMigrationName)] + public class AddUserStartNodeTable : MigrationBase + { + public AddUserStartNodeTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + + if (tables.InvariantContains("umbracoUserStartNode") == false) + { + Create.Table(); + + MigrateUserStartNodes(); + + //now remove the old columns + + Delete.Column("startStructureID").FromTable("umbracoUser"); + Delete.Column("startMediaID").FromTable("umbracoUser"); + } + } + + private void MigrateUserStartNodes() + { + Execute.Sql(@"INSERT INTO umbracoUserStartNode (userId, startNode, startNodeType) + SELECT id, startStructureID, 1 + FROM umbracoUser + WHERE startStructureID IS NOT NULL AND startStructureID > 0 AND startStructureID IN (SELECT id FROM umbracoNode WHERE nodeObjectType='" + Constants.ObjectTypes.Document + "')"); + + Execute.Sql(@"INSERT INTO umbracoUserStartNode (userId, startNode, startNodeType) + SELECT id, startMediaID, 2 + FROM umbracoUser + WHERE startMediaID IS NOT NULL AND startMediaID > 0 AND startMediaID IN (SELECT id FROM umbracoNode WHERE nodeObjectType='" + Constants.ObjectTypes.Media + "')"); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs b/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs index 0d9b1d5552..435b02c2e3 100644 --- a/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs +++ b/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Relators { private UserDto _currentUser; - internal UserDto Map(UserDto user, UserGroupDto group, UserGroup2AppDto section) + internal UserDto Map(UserDto user, UserGroupDto group, UserGroup2AppDto section, UserStartNodeDto startNode) { // Terminating call. Since we can return null from this function // we need to be ready for PetaPoco to callback later with null @@ -20,6 +20,7 @@ namespace Umbraco.Core.Persistence.Relators if (_currentUser != null && _currentUser.Id == user.Id) { AddOrUpdateGroup(group, section); + AddOrUpdateStartNode(startNode); // Return null to indicate we're not done with this object yet return null; @@ -34,13 +35,33 @@ namespace Umbraco.Core.Persistence.Relators // Setup the new current user _currentUser = user; _currentUser.UserGroupDtos = new List(); + _currentUser.UserStartNodeDtos = new List(); AddOrUpdateGroup(group, section); + AddOrUpdateStartNode(startNode); // Return the now populated previous user (or null if first time through) return prev; } + private void AddOrUpdateStartNode(UserStartNodeDto startNode) + { + //this can be null since we are left joining + if (startNode == null || startNode.Id == default(int)) + return; + + //check if this is a new start node + var latestStartNode = _currentUser.UserStartNodeDtos.Count > 0 + ? _currentUser.UserStartNodeDtos[_currentUser.UserStartNodeDtos.Count - 1] + : null; + + if (latestStartNode == null || latestStartNode.Id != startNode.Id) + { + //add the current (new) start node + _currentUser.UserStartNodeDtos.Add(startNode); + } + } + private void AddOrUpdateGroup(UserGroupDto group, UserGroup2AppDto section) { //I don't even think this situation can happen but if it could, we'd want the section added to the latest group check if this is a new group @@ -50,21 +71,21 @@ namespace Umbraco.Core.Persistence.Relators } //this can be null since we are doing a left join - if (group != null && group.Alias.IsNullOrWhiteSpace() == false) - { - //check if this is a new group - var latestGroup = _currentUser.UserGroupDtos.Count > 0 - ? _currentUser.UserGroupDtos[_currentUser.UserGroupDtos.Count - 1] - : null; + if (group == null || group.Alias.IsNullOrWhiteSpace()) + return; - if (latestGroup == null || latestGroup.Id != group.Id) - { - //add the current (new) group - _currentUser.UserGroupDtos.Add(group); - } - - AddSection(section); + //check if this is a new group + var latestGroup = _currentUser.UserGroupDtos.Count > 0 + ? _currentUser.UserGroupDtos[_currentUser.UserGroupDtos.Count - 1] + : null; + + if (latestGroup == null || latestGroup.Id != group.Id) + { + //add the current (new) group + _currentUser.UserGroupDtos.Add(group); } + + AddSection(section); } private void AddSection(UserGroup2AppDto section) diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs index 59335daac0..63a6900634 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs @@ -54,5 +54,8 @@ namespace Umbraco.Core.Persistence.Repositories /// Optional parameter to filter by specfied user state /// IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Expression> orderBy, Direction orderDirection, string[] userGroups = null, UserState? userState = null, IQuery filter = null); + + IProfile GetProfile(string username); + IProfile GetProfile(int id); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 4c203f6dbb..4b05b5a47a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -38,18 +38,45 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where(GetBaseWhereClause(), new { Id = id }); sql //must be included for relator to work .OrderBy(d => d.Id, SqlSyntax) - .OrderBy(d => d.Id, SqlSyntax); + .OrderBy(d => d.Id, SqlSyntax) + .OrderBy(d => d.Id, SqlSyntax); - var dto = Database.Fetch(new UserGroupRelator().Map, sql) + var dto = Database.Fetch(new UserGroupRelator().Map, sql) .FirstOrDefault(); if (dto == null) - return null; + return null; var user = UserFactory.BuildEntity(dto); return user; } - + + public IProfile GetProfile(string username) + { + var sql = GetBaseQuery(false).Where(userDto => userDto.UserName == username, SqlSyntax); + + var dto = Database.Fetch(sql) + .FirstOrDefault(); + + if (dto == null) + return null; + + return new UserProfile(dto.Id, dto.UserName); + } + + public IProfile GetProfile(int id) + { + var sql = GetBaseQuery(false).Where(userDto => userDto.Id == id, SqlSyntax); + + var dto = Database.Fetch(sql) + .FirstOrDefault(); + + if (dto == null) + return null; + + return new UserProfile(dto.Id, dto.UserName); + } + protected override IEnumerable PerformGetAll(params int[] ids) { var sql = GetQueryWithGroups(); @@ -59,9 +86,10 @@ namespace Umbraco.Core.Persistence.Repositories } sql //must be included for relator to work .OrderBy(d => d.Id, SqlSyntax) - .OrderBy(d => d.Id, SqlSyntax); + .OrderBy(d => d.Id, SqlSyntax) + .OrderBy(d => d.Id, SqlSyntax); - var users = ConvertFromDtos(Database.Fetch(new UserGroupRelator().Map, sql)) + var users = ConvertFromDtos(Database.Fetch(new UserGroupRelator().Map, sql)) .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. return users; @@ -74,9 +102,10 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate(); sql //must be included for relator to work .OrderBy(d => d.Id, SqlSyntax) - .OrderBy(d => d.Id, SqlSyntax); + .OrderBy(d => d.Id, SqlSyntax) + .OrderBy(d => d.Id, SqlSyntax); - var dtos = Database.Fetch(new UserGroupRelator().Map, sql) + var dtos = Database.Fetch(new UserGroupRelator().Map, sql) .DistinctBy(x => x.Id); var users = ConvertFromDtos(dtos) @@ -110,7 +139,7 @@ namespace Umbraco.Core.Persistence.Repositories private Sql GetQueryWithGroups() { //base query includes user groups - var sql = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*"); + var sql = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*, umbracoUserStartNode.*"); AddGroupLeftJoin(sql); return sql; } @@ -122,7 +151,9 @@ namespace Umbraco.Core.Persistence.Repositories .LeftJoin(SqlSyntax) .On(SqlSyntax, dto => dto.Id, dto => dto.UserGroupId) .LeftJoin(SqlSyntax) - .On(SqlSyntax, dto => dto.UserGroupId, dto => dto.Id); + .On(SqlSyntax, dto => dto.UserGroupId, dto => dto.Id) + .LeftJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.UserId, dto => dto.Id); } private Sql GetBaseQuery(string columns) @@ -396,6 +427,8 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, mappedField, orderDirection, userGroups, userState, filter); } + + private IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, string[] userGroups = null, UserState? userState = null, IQuery filter = null) { if (string.IsNullOrWhiteSpace(orderBy)) throw new ArgumentException("Value cannot be null or whitespace.", "orderBy"); @@ -448,7 +481,7 @@ namespace Umbraco.Core.Persistence.Repositories string sqlStringCount, sqlStringPage; Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - var sqlQueryFull = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*"); + var sqlQueryFull = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*, umbracoUserStartNode.*"); var fullQueryWithPagedInnerJoin = sqlQueryFull .Append("INNER JOIN (") @@ -464,7 +497,7 @@ namespace Umbraco.Core.Persistence.Repositories GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, filterSql), orderDirection, orderBy); - var users = ConvertFromDtos(Database.Fetch(new UserGroupRelator().Map, fullQuery)) + var users = ConvertFromDtos(Database.Fetch(new UserGroupRelator().Map, fullQuery)) .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. return users; diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 7c2373a000..63880e369e 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -35,8 +35,8 @@ namespace Umbraco.Core.Security AllowedApplications = user.AllowedSections, Culture = user.Culture, Roles = user.Roles.Select(x => x.RoleId).ToArray(), - StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId, + StartContentNodes = user.StartContentIds, + StartMediaNodes = user.StartMediaIds, SessionId = user.SecurityStamp }); diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 4206cc731e..611242b088 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -75,8 +75,8 @@ namespace Umbraco.Core.Security Language = user.Culture ?? Configuration.GlobalSettings.DefaultUILanguage, Name = user.Name, Username = user.UserName, - StartContentId = user.StartContentId == 0 ? -1 : user.StartContentId, - StartMediaId = user.StartMediaId == 0 ? -1 : user.StartMediaId, + StartContentIds = user.StartContentIds ?? new int[] { }, + StartMediaIds = user.StartMediaIds ?? new int[] { }, IsLockedOut = user.IsLockedOut, IsApproved = true }; @@ -673,15 +673,15 @@ namespace Umbraco.Core.Security anythingChanged = true; user.Language = identityUser.Culture; } - if (user.StartMediaId != identityUser.StartMediaId) + if (user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) { anythingChanged = true; - user.StartMediaId = identityUser.StartMediaId; + user.StartMediaIds = identityUser.StartMediaIds; } - if (user.StartContentId != identityUser.StartContentId) + if (user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds)) { anythingChanged = true; - user.StartContentId = identityUser.StartContentId; + user.StartContentIds = identityUser.StartContentIds; } if (user.SecurityStamp != identityUser.SecurityStamp) { diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 1bc9902da5..39931341fa 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -48,12 +48,20 @@ namespace Umbraco.Core.Security || realName == null || session == null) throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since there are missing required claims"); - int startContentIdAsInt; - int startMediaIdAsInt; - if (int.TryParse(startContentId, out startContentIdAsInt) == false || int.TryParse(startMediaId, out startMediaIdAsInt) == false) + int[] startContentIdsAsInt; + int[] startMediaIdsAsInt; + if (startContentId.DetectIsJson() == false || startMediaId.DetectIsJson() == false) + throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly - either content or media start Ids are not JSON"); + + try { - throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly"); + startContentIdsAsInt = JsonConvert.DeserializeObject(startContentId); + startMediaIdsAsInt = JsonConvert.DeserializeObject(startMediaId); } + catch (Exception e) + { + throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly - either content or media start Ids could not be parsed as JSON", e); + } var roles = identity.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToList(); var allowedApps = identity.FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToList(); @@ -67,8 +75,8 @@ namespace Umbraco.Core.Security Roles = roles.ToArray(), Username = username, RealName = realName, - StartContentNode = startContentIdAsInt, - StartMediaNode = startMediaIdAsInt + StartContentNodes = startContentIdsAsInt, + StartMediaNodes = startMediaIdsAsInt }; return new UmbracoBackOfficeIdentity(identity, userData); @@ -202,10 +210,10 @@ namespace Umbraco.Core.Security AddClaim(new Claim(ClaimTypes.GivenName, UserData.RealName, ClaimValueTypes.String, Issuer, Issuer, this)); if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false) - AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, JsonConvert.SerializeObject(StartContentNodes), ClaimValueTypes.Integer32, Issuer, Issuer, this)); if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false) - AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, JsonConvert.SerializeObject(StartMediaNodes), ClaimValueTypes.Integer32, Issuer, Issuer, this)); if (HasClaim(x => x.Type == ClaimTypes.Locality) == false) AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this)); @@ -259,14 +267,14 @@ namespace Umbraco.Core.Security get { return _currentIssuer; } } - public int StartContentNode + public int[] StartContentNodes { - get { return UserData.StartContentNode; } + get { return UserData.StartContentNodes; } } - public int StartMediaNode + public int[] StartMediaNodes { - get { return UserData.StartMediaNode; } + get { return UserData.StartMediaNodes; } } public string[] AllowedApplications diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs index 407d2782dd..31a7d50f25 100644 --- a/src/Umbraco.Core/Security/UserData.cs +++ b/src/Umbraco.Core/Security/UserData.cs @@ -48,10 +48,10 @@ namespace Umbraco.Core.Security public string RealName { get; set; } [DataMember(Name = "startContent")] - public int StartContentNode { get; set; } + public int[] StartContentNodes { get; set; } [DataMember(Name = "startMedia")] - public int StartMediaNode { get; set; } + public int[] StartMediaNodes { get; set; } [DataMember(Name = "allowedApps")] public string[] AllowedApplications { get; set; } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 8072ecb94d..a801986b60 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -107,9 +107,7 @@ namespace Umbraco.Core.Services Language = GlobalSettings.DefaultUILanguage, Name = username, RawPasswordValue = passwordValue, - Username = username, - StartContentId = -1, - StartMediaId = -1, + Username = username, IsLockedOut = false, IsApproved = true }; @@ -604,8 +602,11 @@ namespace Umbraco.Core.Services /// public IProfile GetProfileById(int id) { - var user = GetUserById(id); - return user.ProfileData; + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateUserRepository(uow); + return repository.GetProfile(id); + } } /// @@ -615,8 +616,11 @@ namespace Umbraco.Core.Services /// public IProfile GetProfileByUserName(string username) { - var user = GetByUsername(username); - return user.ProfileData; + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateUserRepository(uow); + return repository.GetProfile(username); + } } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b6728affdf..52a367224b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -343,8 +343,10 @@ + + @@ -510,6 +512,7 @@ + diff --git a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Controllers/UsersControllerTests.cs index b722bc789b..7af3d5e340 100644 --- a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Controllers/UsersControllerTests.cs @@ -36,6 +36,52 @@ namespace Umbraco.Tests.Controllers public class UsersControllerTests : BaseDatabaseFactoryTest { + [Test] + public async void Save_User() + { + var runner = new TestRunner((message, helper) => + { + //setup some mocks + Umbraco.Core.Configuration.GlobalSettings.HasSmtpServer = true; + + var userServiceMock = Mock.Get(helper.UmbracoContext.Application.Services.UserService); + + userServiceMock.Setup(service => service.Save(It.IsAny(), It.IsAny())) + .Callback((IUser u, bool raiseEvents) => + { + u.Id = 1234; + }); + userServiceMock.Setup(service => service.GetAllUserGroups(It.IsAny())) + .Returns(Enumerable.Empty); + + //we need to manually apply automapper mappings with the mocked applicationcontext + InitializeMappers(helper.UmbracoContext.Application); + + return new UsersController(helper.UmbracoContext); + }); + + var invite = new UserInvite + { + Id = -1, + Email = "test@test.com", + Message = "Hello test!", + Name = "Test", + UserGroups = new[] { "writers" } + }; + var response = await runner.Execute("Users", "PostSaveUser", HttpMethod.Post, + new ObjectContent(invite, new JsonMediaTypeFormatter())); + + var obj = JsonConvert.DeserializeObject(response.Item2); + + Assert.AreEqual(invite.Name, obj.Name); + Assert.AreEqual(1234, obj.Id); + Assert.AreEqual(invite.Email, obj.Email); + foreach (var group in invite.UserGroups) + { + Assert.IsTrue(obj.UserGroups.Contains(group)); + } + } + [Test] public async void Invite_User() { diff --git a/src/Umbraco.Tests/Models/UserExtensionsTests.cs b/src/Umbraco.Tests/Models/UserExtensionsTests.cs index 84537987c9..c6f9c55ead 100644 --- a/src/Umbraco.Tests/Models/UserExtensionsTests.cs +++ b/src/Umbraco.Tests/Models/UserExtensionsTests.cs @@ -17,7 +17,7 @@ namespace Umbraco.Tests.Models public void Determines_Path_Based_Access_To_Content(int userId, string contentPath, bool outcome) { var userMock = new Mock(); - userMock.Setup(u => u.StartContentId).Returns(userId); + userMock.Setup(u => u.StartContentIds).Returns(new[]{ userId }); var user = userMock.Object; var contentMock = new Mock(); contentMock.Setup(c => c.Path).Returns(contentPath); diff --git a/src/Umbraco.Tests/Models/UserTests.cs b/src/Umbraco.Tests/Models/UserTests.cs index 5bdc842cc1..104f435bf5 100644 --- a/src/Umbraco.Tests/Models/UserTests.cs +++ b/src/Umbraco.Tests/Models/UserTests.cs @@ -35,8 +35,8 @@ namespace Umbraco.Tests.Models PasswordQuestion = "question", //ProviderUserKey = "user key", SessionTimeout = 5, - StartContentId = 3, - StartMediaId = 8, + StartContentIds = new []{ 3 }, + StartMediaIds = new[]{ 8 }, Username = "username" }; @@ -82,8 +82,8 @@ namespace Umbraco.Tests.Models PasswordQuestion = "question", //ProviderUserKey = "user key", SessionTimeout = 5, - StartContentId = 3, - StartMediaId = 8, + StartContentIds = new[]{ 3 }, + StartMediaIds = new []{ 8 }, Username = "username" }; diff --git a/src/Umbraco.Tests/Persistence/Repositories/NotificationsRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/NotificationsRepositoryTest.cs index e9d9163b12..9f336d404e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/NotificationsRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/NotificationsRepositoryTest.cs @@ -45,7 +45,7 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repo = new NotificationsRepository(unitOfWork)) { - var userDto = new UserDto {ContentStartId = -1, Email = "test", Login = "test", MediaStartId = -1, Password = "test", UserName = "test", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; + var userDto = new UserDto {Email = "test", Login = "test", Password = "test", UserName = "test", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; unitOfWork.Database.Insert(userDto); var userNew = Mock.Of(e => e.Id == userDto.Id); @@ -81,7 +81,7 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 10; i++) { - var userDto = new UserDto {ContentStartId = -1, Email = "test" + i, Login = "test" + i, MediaStartId = -1, Password = "test", UserName = "test" + i, UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now}; + var userDto = new UserDto {Email = "test" + i, Login = "test" + i, Password = "test", UserName = "test" + i, UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now}; unitOfWork.Database.Insert(userDto); var userNew = Mock.Of(e => e.Id == userDto.Id); var notification = repo.CreateNotification(userNew, (i % 2 == 0) ? entity1 : entity2, i.ToString(CultureInfo.InvariantCulture)); @@ -109,7 +109,7 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 10; i++) { - var userDto = new UserDto {ContentStartId = -1, Email = "test" + i, Login = "test" + i, MediaStartId = -1, Password = "test", UserName = "test" + i, UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now}; + var userDto = new UserDto {Email = "test" + i, Login = "test" + i, Password = "test", UserName = "test" + i, UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now}; unitOfWork.Database.Insert(userDto); var userNew = Mock.Of(e => e.Id == userDto.Id); var notification = repo.CreateNotification(userNew, (i % 2 == 0) ? entity1 : entity2, i.ToString(CultureInfo.InvariantCulture)); @@ -128,7 +128,7 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repo = new NotificationsRepository(unitOfWork)) { - var userDto = new UserDto {ContentStartId = -1, Email = "test", Login = "test", MediaStartId = -1, Password = "test", UserName = "test", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; + var userDto = new UserDto {Email = "test", Login = "test", Password = "test", UserName = "test", UserLanguage = "en", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }; unitOfWork.Database.Insert(userDto); var userNew = Mock.Of(e => e.Id == userDto.Id); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 0ad8fe1708..a4cf866cd2 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -153,8 +153,8 @@ namespace Umbraco.Tests.Persistence.Repositories resolved.IsApproved = false; resolved.RawPasswordValue = "new"; resolved.IsLockedOut = true; - resolved.StartContentId = 10; - resolved.StartMediaId = 11; + resolved.StartContentIds = new []{ 10 }; + resolved.StartMediaIds = new []{ 11 }; resolved.Email = "new@new.com"; resolved.Username = "newName"; @@ -169,8 +169,8 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(updatedItem.IsApproved, Is.EqualTo(resolved.IsApproved)); Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(resolved.RawPasswordValue)); Assert.That(updatedItem.IsLockedOut, Is.EqualTo(resolved.IsLockedOut)); - Assert.That(updatedItem.StartContentId, Is.EqualTo(resolved.StartContentId)); - Assert.That(updatedItem.StartMediaId, Is.EqualTo(resolved.StartMediaId)); + Assert.IsTrue(updatedItem.StartContentIds.UnsortedSequenceEqual(resolved.StartContentIds)); + Assert.IsTrue(updatedItem.StartMediaIds.UnsortedSequenceEqual(resolved.StartMediaIds)); Assert.That(updatedItem.Email, Is.EqualTo(resolved.Email)); Assert.That(updatedItem.Username, Is.EqualTo(resolved.Username)); Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(2)); @@ -360,8 +360,8 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); Assert.That(updatedItem.IsLockedOut, Is.EqualTo(originalUser.IsLockedOut)); - Assert.That(updatedItem.StartContentId, Is.EqualTo(originalUser.StartContentId)); - Assert.That(updatedItem.StartMediaId, Is.EqualTo(originalUser.StartMediaId)); + Assert.IsTrue(updatedItem.StartContentIds.UnsortedSequenceEqual(originalUser.StartContentIds)); + Assert.IsTrue(updatedItem.StartMediaIds.UnsortedSequenceEqual(originalUser.StartMediaIds)); Assert.That(updatedItem.Email, Is.EqualTo(originalUser.Email)); Assert.That(updatedItem.Username, Is.EqualTo(originalUser.Username)); Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(2)); diff --git a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs index 813653fb7c..226b26990d 100644 --- a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs @@ -44,8 +44,8 @@ namespace Umbraco.Tests.Security Assert.AreEqual(sessionId, backofficeIdentity.SessionId); Assert.AreEqual("testing", backofficeIdentity.Username); Assert.AreEqual("hello world", backofficeIdentity.RealName); - Assert.AreEqual(-1, backofficeIdentity.StartContentNode); - Assert.AreEqual(5543, backofficeIdentity.StartMediaNode); + Assert.AreEqual(0, backofficeIdentity.StartContentNodes.Length); + Assert.IsTrue(backofficeIdentity.StartMediaNodes.UnsortedSequenceEqual(new []{ 5543 })); Assert.IsTrue(new[] {"content", "media"}.SequenceEqual(backofficeIdentity.AllowedApplications)); Assert.AreEqual("en-us", backofficeIdentity.Culture); Assert.IsTrue(new[] { "admin" }.SequenceEqual(backofficeIdentity.Roles)); @@ -98,8 +98,7 @@ namespace Umbraco.Tests.Security Id = 1234, RealName = "hello world", Roles = new[] {"admin"}, - StartContentNode = -1, - StartMediaNode = 654, + StartMediaNodes = new []{ 654 }, Username = "testing" }; @@ -119,8 +118,7 @@ namespace Umbraco.Tests.Security Id = 1234, RealName = "hello world", Roles = new[] { "admin" }, - StartContentNode = -1, - StartMediaNode = 654, + StartMediaNodes =new []{ 654 } , Username = "testing" }; @@ -146,8 +144,7 @@ namespace Umbraco.Tests.Security Id = 1234, RealName = "hello world", Roles = new[] { "admin" }, - StartContentNode = -1, - StartMediaNode = 654, + StartMediaNodes = new []{ 654 }, Username = "testing" }; @@ -170,8 +167,7 @@ namespace Umbraco.Tests.Security Id = 1234, RealName = "hello world", Roles = new[] { "admin" }, - StartContentNode = -1, - StartMediaNode = 654, + StartMediaNodes = new []{ 654 }, Username = "testing" }; diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index d99f6e70d2..5a0ad30169 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using umbraco.BusinessLogic.Actions; +using Umbraco.Core; using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Services @@ -684,8 +685,8 @@ namespace Umbraco.Tests.Services Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); Assert.That(updatedItem.IsLockedOut, Is.EqualTo(originalUser.IsLockedOut)); - Assert.That(updatedItem.StartContentId, Is.EqualTo(originalUser.StartContentId)); - Assert.That(updatedItem.StartMediaId, Is.EqualTo(originalUser.StartMediaId)); + Assert.IsTrue(updatedItem.StartContentIds.UnsortedSequenceEqual(originalUser.StartContentIds)); + Assert.IsTrue(updatedItem.StartMediaIds.UnsortedSequenceEqual(originalUser.StartMediaIds)); Assert.That(updatedItem.Email, Is.EqualTo(originalUser.Email)); Assert.That(updatedItem.Username, Is.EqualTo(originalUser.Username)); Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(2)); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs index 2dbfa757f2..0cfcb47ade 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs @@ -31,9 +31,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting Roles = new[] { "admin" }, AllowedApplications = new[] { "content", "media", "members" }, Culture = "en-US", - RealName = "Admin", - StartContentNode = -1, - StartMediaNode = -1, + RealName = "Admin", Username = "admin" }); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index 7299d9758e..127269a384 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -90,8 +90,8 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting && u.Id == (int) backofficeIdentity.Id && u.Language == "en" && u.Name == backofficeIdentity.RealName - && u.StartContentId == backofficeIdentity.StartContentNode - && u.StartMediaId == backofficeIdentity.StartMediaNode + && u.StartContentIds == backofficeIdentity.StartContentNodes + && u.StartMediaIds == backofficeIdentity.StartMediaNodes && u.Username == backofficeIdentity.Username)); //mock Validate diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs index 53b53e634f..ce2931e99e 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedUser.cs @@ -15,8 +15,6 @@ namespace Umbraco.Tests.TestHelpers.Entities Name = "TestUser" + suffix, RawPasswordValue = "testing", IsLockedOut = false, - StartContentId = -1, - StartMediaId = -1, Email = "test" + suffix + "@test.com", Username = "TestUser" + suffix }; diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index c7922b9d58..2b832c098c 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -96,7 +96,7 @@ namespace Umbraco.Tests.UmbracoExamine } if (userService == null) { - userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == (object)0 && p.Name == "admin")); + userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == 0 && p.Name == "admin")); } if (mediaService == null) { diff --git a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs index 9634bb164a..7782612420 100644 --- a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var contentMock = new Mock(); contentMock.Setup(c => c.Path).Returns("-1,1234,5678"); @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var contentMock = new Mock(); contentMock.Setup(c => c.Path).Returns("-1,1234,5678"); @@ -63,7 +63,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartContentId).Returns(9876); + userMock.Setup(u => u.StartContentIds).Returns(new[]{ 9876 }); var user = userMock.Object; var contentMock = new Mock(); contentMock.Setup(c => c.Path).Returns("-1,1234,5678"); @@ -89,7 +89,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var contentMock = new Mock(); contentMock.Setup(c => c.Path).Returns("-1,1234,5678"); @@ -118,7 +118,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var contentMock = new Mock(); contentMock.Setup(c => c.Path).Returns("-1,1234,5678"); @@ -147,7 +147,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; //act @@ -163,7 +163,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; //act @@ -179,7 +179,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartContentId).Returns(1234); + userMock.Setup(u => u.StartContentIds).Returns(new []{ 1234 }); var user = userMock.Object; //act @@ -195,7 +195,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartContentId).Returns(1234); + userMock.Setup(u => u.StartContentIds).Returns(new []{ 1234 }); var user = userMock.Object; //act @@ -211,7 +211,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var userServiceMock = new Mock(); @@ -235,7 +235,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var userServiceMock = new Mock(); @@ -259,7 +259,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var userServiceMock = new Mock(); @@ -283,7 +283,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var userServiceMock = new Mock(); diff --git a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs index c776924af7..bc4046b39e 100644 --- a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs @@ -83,7 +83,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartContentId).Returns(5); + userMock.Setup(u => u.StartContentIds).Returns(new []{ 5 }); var user = userMock.Object; att.FilterBasedOnStartNode(list, user); @@ -105,7 +105,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartContentId).Returns(-1); + userMock.Setup(u => u.StartContentIds).Returns(new int[0]); var user = userMock.Object; var userServiceMock = new Mock(); diff --git a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/MediaControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/MediaControllerUnitTests.cs index 2d5420409c..e65b1a1b19 100644 --- a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/MediaControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/WebApiEditors/MediaControllerUnitTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartMediaId).Returns(-1); + userMock.Setup(u => u.StartMediaIds).Returns(new int[0]); var user = userMock.Object; var mediaMock = new Mock(); mediaMock.Setup(m => m.Path).Returns("-1,1234,5678"); @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartMediaId).Returns(-1); + userMock.Setup(u => u.StartMediaIds).Returns(new int[0]); var user = userMock.Object; var mediaMock = new Mock(); mediaMock.Setup(m => m.Path).Returns("-1,1234,5678"); @@ -59,7 +59,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); - userMock.Setup(u => u.StartMediaId).Returns(9876); + userMock.Setup(u => u.StartMediaIds).Returns(new[]{ 9876 }); var user = userMock.Object; var mediaMock = new Mock(); mediaMock.Setup(m => m.Path).Returns("-1,1234,5678"); @@ -81,7 +81,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartMediaId).Returns(-1); + userMock.Setup(u => u.StartMediaIds).Returns(new int[]{}); var user = userMock.Object; //act @@ -97,7 +97,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartMediaId).Returns(1234); + userMock.Setup(u => u.StartMediaIds).Returns(new[]{ 1234 }); var user = userMock.Object; //act @@ -113,7 +113,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartMediaId).Returns(-1); + userMock.Setup(u => u.StartMediaIds).Returns(new int[]{}); var user = userMock.Object; //act @@ -129,7 +129,7 @@ namespace Umbraco.Tests.Web.Controllers.WebApiEditors //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); - userMock.Setup(u => u.StartMediaId).Returns(1234); + userMock.Setup(u => u.StartMediaIds).Returns(new[]{ 1234 }); var user = userMock.Object; //act diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index c95e1a7b7f..1a652a787c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -26,7 +26,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat enablelistviewexpand: '@' }, - compile: function(element, attrs) { + compile: function (element, attrs) { //config //var showheader = (attrs.showheader !== 'false'); var hideoptions = (attrs.hideoptions === 'true') ? "hide-options" : ""; @@ -44,7 +44,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat element.replaceWith(template); - return function(scope, elem, attr, controller) { + return function (scope, elem, attr, controller) { //flag to track the last loaded section when the tree 'un-loads'. We use this to determine if we should // re-load the tree again. For example, if we hover over 'content' the content tree is shown. Then we hover @@ -84,16 +84,16 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat function setupExternalEvents() { if (scope.eventhandler) { - scope.eventhandler.clearCache = function(section) { + scope.eventhandler.clearCache = function (section) { treeService.clearCache({ section: section }); }; - scope.eventhandler.load = function(section) { + scope.eventhandler.load = function (section) { scope.section = section; loadTree(); }; - scope.eventhandler.reloadNode = function(node) { + scope.eventhandler.reloadNode = function (node) { if (!node) { node = scope.currentNode; @@ -108,7 +108,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat Used to do the tree syncing. If the args.tree is not specified we are assuming it has been specified previously using the _setActiveTreeType */ - scope.eventhandler.syncTree = function(args) { + scope.eventhandler.syncTree = function (args) { if (!args) { throw "args cannot be null"; } @@ -146,9 +146,16 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat // and previous so that the tree syncs properly. The tree syncs from the top down and if there are parts // of the tree's path in there that don't actually exist in the dom/model then syncing will not work. - userService.getCurrentUser().then(function(userData) { + userService.getCurrentUser().then(function (userData) { + + var startNodes = []; + for (var i = 0; i < userData.startContentIds; i++) { + startNodes.push(userData.startContentIds[i]); + } + for (var j = 0; j < userData.startMediaIds; j++) { + startNodes.push(userData.startMediaIds[j]); + } - var startNodes = [userData.startContentId, userData.startMediaId]; _.each(startNodes, function (i) { var found = _.find(args.path, function (p) { return String(p) === String(i); @@ -175,7 +182,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat node's children - this is synonymous with the legacy refreshTree method - again should not be used and should only be used for the legacy code to work. */ - scope.eventhandler._setActiveTreeType = function(treeAlias, loadChildren) { + scope.eventhandler._setActiveTreeType = function (treeAlias, loadChildren) { loadActiveTree(treeAlias, loadChildren); }; } @@ -210,7 +217,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat function doLoad(tree) { var childrenAndSelf = [tree].concat(tree.children); scope.activeTree = _.find(childrenAndSelf, function (node) { - if(node && node.metaData && node.metaData.treeAlias) { + if (node && node.metaData && node.metaData.treeAlias) { return node.metaData.treeAlias.toUpperCase() === treeAlias.toUpperCase(); } return false; @@ -223,7 +230,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat //This is only used for the legacy tree method refreshTree! if (loadChildren) { scope.activeTree.expanded = true; - scope.loadChildren(scope.activeTree, false).then(function() { + scope.loadChildren(scope.activeTree, false).then(function () { emitEvent("activeTreeLoaded", { tree: scope.activeTree }); }); } @@ -236,7 +243,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat doLoad(scope.tree.root); } else { - scope.eventhandler.one("treeLoaded", function(e, args) { + scope.eventhandler.one("treeLoaded", function (e, args) { doLoad(args.tree.root); }); } @@ -261,7 +268,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat } treeService.getTree(args) - .then(function(data) { + .then(function (data) { //set the data once we have it scope.tree = data; @@ -274,7 +281,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat emitEvent("treeLoaded", { tree: scope.tree }); emitEvent("treeNodeExpanded", { tree: scope.tree, node: scope.tree.root, children: scope.tree.root.children }); - }, function(reason) { + }, function (reason) { scope.loading = false; notificationsService.error("Tree Error", reason); }); @@ -306,8 +313,8 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat scope.selectEnabledNodeClass = function (node) { return node ? node.selected ? - 'icon umb-tree-icon sprTree icon-check green temporary' : - '' : + 'icon umb-tree-icon sprTree icon-check green temporary' : + '' : ''; }; @@ -315,7 +322,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat * This changes dynamically based on if we are changing sections or just loading normal tree data. * When changing sections we don't want all of the tree-ndoes to do their 'leave' animations. */ - scope.animation = function() { + scope.animation = function () { if (deleteAnimations && scope.tree && scope.tree.root && scope.tree.root.expanded) { return { leave: 'tree-node-delete-leave' }; } @@ -325,7 +332,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat }; /* helper to force reloading children of a tree node */ - scope.loadChildren = function(node, forceReload) { + scope.loadChildren = function (node, forceReload) { var deferred = $q.defer(); //emit treeNodeExpanding event, if a callback object is set on the tree @@ -339,7 +346,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat if (forceReload || (node.hasChildren && node.children.length === 0)) { //get the children from the tree service treeService.loadNodeChildren({ node: node, section: scope.section }) - .then(function(data) { + .then(function (data) { //emit expanded event emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: data }); @@ -365,7 +372,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat The tree doesnt know about this, so it raises an event to tell the parent controller about it. */ - scope.options = function(n, ev) { + scope.options = function (n, ev) { emitEvent("treeOptionsClick", { element: elem, node: n, event: ev }); }; @@ -384,12 +391,12 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat emitEvent("treeNodeSelect", { element: elem, node: n, event: ev }); }; - scope.altSelect = function(n, ev) { + scope.altSelect = function (n, ev) { emitEvent("treeNodeAltSelect", { element: elem, tree: scope.tree, node: n, event: ev }); }; //watch for section changes - scope.$watch("section", function(newVal, oldVal) { + scope.$watch("section", function (newVal, oldVal) { if (!scope.tree) { loadTree(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 262b06dd50..7b7ea7e3b4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -235,16 +235,6 @@ angular.module('umbraco.services') var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "implicit" }; - //TODO: This is a mega backwards compatibility hack... These variables SHOULD NOT exist in the server variables - // since they are not supposed to be dynamic but I accidentally added them there in 7.1.5 IIRC so some people might - // now be relying on this :( - if (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables) { - Umbraco.Sys.ServerVariables["security"] = { - startContentId: data.startContentId, - startMediaId: data.startMediaId - }; - } - if (args && args.broadcastEvent) { //broadcast a global event, will inform listening controllers to load in the user specific data eventsService.emit("app.authenticated", result); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js index 5ee103a32b..e76db90f47 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js @@ -108,15 +108,15 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", $scope.switchToMediaPicker = function () { userService.getCurrentUser().then(function (userData) { - dialogService.mediaPicker({ - startNodeId: userData.startMediaId, - callback: function (media) { - $scope.target.id = media.id; - $scope.target.isMedia = true; - $scope.target.name = media.name; - $scope.target.url = mediaHelper.resolveFile(media); - } - }); + dialogService.mediaPicker({ + startNodeId: userData.startMediaIds.length == 0 ? -1 : userData.startMediaIds[0], + callback: function(media) { + $scope.target.id = media.id; + $scope.target.isMedia = true; + $scope.target.name = media.name; + $scope.target.url = mediaHelper.resolveFile(media); + } + }); }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index fde3e4cab3..9c5a2b888c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -89,23 +89,23 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", $scope.switchToMediaPicker = function () { userService.getCurrentUser().then(function (userData) { - $scope.mediaPickerOverlay = { - view: "mediapicker", - startNodeId: userData.startMediaId, - show: true, - submit: function(model) { - var media = model.selectedImages[0]; + $scope.mediaPickerOverlay = { + view: "mediapicker", + startNodeId: userData.startMediaIds.length == 0 ? -1 : userData.startMediaIds[0], + show: true, + submit: function(model) { + var media = model.selectedImages[0]; - $scope.model.target.id = media.id; - $scope.model.target.udi = media.udi; - $scope.model.target.isMedia = true; - $scope.model.target.name = media.name; - $scope.model.target.url = mediaHelper.resolveFile(media); + $scope.model.target.id = media.id; + $scope.model.target.udi = media.udi; + $scope.model.target.isMedia = true; + $scope.model.target.name = media.name; + $scope.model.target.url = mediaHelper.resolveFile(media); - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - } - }; + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } + }; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js index 9b7c929367..b0b19a2716 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js @@ -279,7 +279,7 @@ function MediaFolderBrowserDashboardController($rootScope, $scope, $location, co currentUser = user; // check if the user start node is the dashboard - if(currentUser.startMediaId === -1) { + if (currentUser.startMediaIds.length === 0 || currentUser.startMediaIds.indexOf(-1) >= 0) { //get the system media listview contentTypeResource.getPropertyTypeScaffold(-96) @@ -305,7 +305,7 @@ function MediaFolderBrowserDashboardController($rootScope, $scope, $location, co } else { // redirect to start node - $location.path("/media/media/edit/" + currentUser.startMediaId); + $location.path("/media/media/edit/" + (currentUser.startMediaIds.length === 0 ? -1 : currentUser.startMediaIds[0])); } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index bb580a906d..57f7d06f80 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -4,7 +4,7 @@ angular.module("umbraco") if (!$scope.model.config.startNodeId) { userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaId; + $scope.model.config.startNodeId = userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0]; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index e406f87aed..6ec9e74fa4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -28,7 +28,7 @@ currentTarget: currentTarget, onlyImages: true, showDetails: true, - startNodeId: userData.startMediaId, + startNodeId: userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0], view: "mediapicker", show: true, submit: function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 6652c52cc0..d09c89c445 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -10,7 +10,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl if (!$scope.model.config.startNodeId) { userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaId; + $scope.model.config.startNodeId = userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0]; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index bbb8024a53..45e8856132 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -250,7 +250,7 @@ angular.module("umbraco") onlyImages: true, showDetails: true, disableFolderSelect: true, - startNodeId: userData.startMediaId, + startNodeId: userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0], view: "mediapicker", show: true, submit: function(model) { diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index da5e0c3a3b..6682b22389 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -800,16 +800,16 @@ namespace Umbraco.Web.Editors } var hasPathAccess = (nodeId == Constants.System.Root) - ? UserExtensions.HasPathAccess( - Constants.System.Root.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : (nodeId == Constants.System.RecycleBinContent) - ? UserExtensions.HasPathAccess( - Constants.System.RecycleBinContent.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : user.HasPathAccess(contentItem); + ? UserExtensions.HasPathAccess( + Constants.System.Root.ToInvariantString(), + user.StartContentIds, + Constants.System.RecycleBinContent) + : (nodeId == Constants.System.RecycleBinContent) + ? UserExtensions.HasPathAccess( + Constants.System.RecycleBinContent.ToInvariantString(), + user.StartContentIds, + Constants.System.RecycleBinContent) + : user.HasPathAccess(contentItem); if (hasPathAccess == false) { diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 48da53e263..e16d6da67a 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -597,6 +597,36 @@ namespace Umbraco.Web.Editors return ExamineSearch(query, entityType, 200, 0, out total, searchFrom); } + private void AddExamineSearchFrom(string searchFrom, StringBuilder sb) + { + //if searchFrom is specified and it is greater than 0 + int mediaSearchFrom; + if (searchFrom != null && int.TryParse(searchFrom, out mediaSearchFrom) && mediaSearchFrom > 0) + { + sb.Append("+__Path: \\-1*\\,"); + sb.Append(mediaSearchFrom.ToString(CultureInfo.InvariantCulture)); + sb.Append("\\,* "); + } + } + + private void AddExamineUserStartNode(int[] startNodes, StringBuilder sb) + { + //make sure only what the user is configured to view is found + if (startNodes.Length > 0) + sb.Append("+("); + foreach (var startNode in startNodes) + { + if (startNode > 0) + { + sb.Append("__Path: \\-1*\\,"); + sb.Append(startNode.ToString(CultureInfo.InvariantCulture)); + sb.Append("\\,* "); + } + } + if (startNodes.Length > 0) + sb.Append(")"); + } + /// /// Searches for results based on the entity type /// @@ -636,34 +666,16 @@ namespace Umbraco.Web.Editors case UmbracoEntityTypes.Media: type = "media"; - var mediaSearchFrom = int.MinValue; - - if (Security.CurrentUser.StartMediaId > 0 || - //if searchFrom is specified and it is greater than 0 - (searchFrom != null && int.TryParse(searchFrom, out mediaSearchFrom) && mediaSearchFrom > 0)) - { - sb.Append("+__Path: \\-1*\\,"); - sb.Append(mediaSearchFrom > 0 - ? mediaSearchFrom.ToString(CultureInfo.InvariantCulture) - : Security.CurrentUser.StartMediaId.ToString(CultureInfo.InvariantCulture)); - sb.Append("\\,* "); - } + AddExamineSearchFrom(searchFrom, sb); + AddExamineUserStartNode(Security.CurrentUser.StartMediaIds, sb); + break; case UmbracoEntityTypes.Document: type = "content"; - var contentSearchFrom = int.MinValue; - - if (Security.CurrentUser.StartContentId > 0 || - //if searchFrom is specified and it is greater than 0 - (searchFrom != null && int.TryParse(searchFrom, out contentSearchFrom) && contentSearchFrom > 0)) - { - sb.Append("+__Path: \\-1*\\,"); - sb.Append(contentSearchFrom > 0 - ? contentSearchFrom.ToString(CultureInfo.InvariantCulture) - : Security.CurrentUser.StartContentId.ToString(CultureInfo.InvariantCulture)); - sb.Append("\\,* "); - } + AddExamineSearchFrom(searchFrom, sb); + AddExamineUserStartNode(Security.CurrentUser.StartContentIds, sb); + break; default: throw new NotSupportedException("The " + typeof(EntityController) + " currently does not support searching against object type " + entityType); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 4980605374..4cac919b1b 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -884,16 +884,16 @@ namespace Umbraco.Web.Editors } var hasPathAccess = (nodeId == Constants.System.Root) - ? UserExtensions.HasPathAccess( - Constants.System.Root.ToInvariantString(), - user.StartMediaId, - Constants.System.RecycleBinMedia) - : (nodeId == Constants.System.RecycleBinMedia) - ? UserExtensions.HasPathAccess( - Constants.System.RecycleBinMedia.ToInvariantString(), - user.StartMediaId, - Constants.System.RecycleBinMedia) - : user.HasPathAccess(media); + ? UserExtensions.HasPathAccess( + Constants.System.Root.ToInvariantString(), + user.StartMediaIds, + Constants.System.RecycleBinMedia) + : (nodeId == Constants.System.RecycleBinMedia) + ? UserExtensions.HasPathAccess( + Constants.System.RecycleBinMedia.ToInvariantString(), + user.StartMediaIds, + Constants.System.RecycleBinMedia) + : user.HasPathAccess(media); return hasPathAccess; } diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index c6ce2ecc81..73757f5427 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -98,6 +98,34 @@ namespace Umbraco.Web.Editors }; } + /// + /// Creates a new user + /// + /// + /// + public UserDisplay PostCreateUser(UserInvite userSave) + { + if (userSave == null) throw new ArgumentNullException("userSave"); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var existing = Services.UserService.GetByEmail(userSave.Email); + if (existing != null) + { + ModelState.AddModelError("Email", "A user with the email already exists"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var user = Mapper.Map(userSave); + + Services.UserService.Save(user); + + return Mapper.Map(user); + } + /// /// Invites a user /// @@ -158,8 +186,31 @@ namespace Umbraco.Web.Editors if (found == null) throw new HttpResponseException(HttpStatusCode.NotFound); + var hasErrors = false; + + var existing = Services.UserService.GetByEmail(userSave.Email); + if (existing != null && existing.Id != (int)userSave.Id) + { + ModelState.AddModelError("Email", "A user with the email already exists"); + hasErrors = true; + } + existing = Services.UserService.GetByUsername(userSave.Name); + if (existing != null && existing.Id != (int)userSave.Id) + { + ModelState.AddModelError("Email", "A user with the email already exists"); + hasErrors = true; + } + + if (hasErrors) + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + //TODO: More validation, password changing logic, persisting - return Mapper.Map(found); + + var user = Mapper.Map(userSave); + + Services.UserService.Save(user); + + return Mapper.Map(user); } /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index 9b29e132af..3cdf1cc3b6 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -30,11 +30,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "remainingAuthSeconds")] public double SecondsUntilTimeout { get; set; } - [DataMember(Name = "startContentId")] - public int StartContentId { get; set; } + [DataMember(Name = "startContentIds")] + public int[] StartContentIds { get; set; } - [DataMember(Name = "startMediaId")] - public int StartMediaId { get; set; } + [DataMember(Name = "startMediaIds")] + public int[] StartMediaIds { get; set; } /// /// A list of sections the user is allowed to view. diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index a0c2a8ef33..e4bbe7850b 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -48,10 +48,9 @@ namespace Umbraco.Web.Models.ContentEditing /// /// Gets the available user groups (i.e. to populate a drop down) - /// The key is the Alias the value is the Name - the Alias is what is used in the UserGroup property and for persistence /// [DataMember(Name = "availableUserGroups")] - public IDictionary AvailableUserGroups { get; set; } + public IEnumerable AvailableUserGroups { get; set; } /// /// Gets the available cultures (i.e. to populate a drop down) @@ -60,26 +59,17 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "availableCultures")] public IDictionary AvailableCultures { get; set; } - //TODO: This will become StartContentIds as an array! - [DataMember(Name = "startContentId")] - public int StartContentId { get; set; } + [DataMember(Name = "startContentIds")] + public int[] StartContentIds { get; set; } - //TODO: This will become StartMediaIds as an array! - [DataMember(Name = "startMediaId")] - public int StartMediaId { get; set; } + [DataMember(Name = "startMediaIds")] + public int[] StartMediaIds { get; set; } - /// - /// A list of sections the user is allowed to view. - /// - [DataMember(Name = "allowedSections")] - public IEnumerable AllowedSections { get; set; } - - /// - /// Gets the available sections (i.e. to populate a drop down) - /// The key is the Alias the value is the Name - the Alias is what is used in the AllowedSections property and for persistence - /// - [DataMember(Name = "availableSections")] - public IDictionary AvailableSections { get; set; } + ///// + ///// A list of sections the user is allowed to view based on their current groups assigned + ///// + //[DataMember(Name = "allowedSections")] + //public IEnumerable AllowedSections { get; set; } /// /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. diff --git a/src/Umbraco.Web/Models/ContentEditing/UserGroupDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserGroupDisplay.cs index 06e24b7d38..404b9925f9 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserGroupDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserGroupDisplay.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.Models.ContentEditing public UserGroupDisplay() { Notifications = new List(); - } + } /// /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. @@ -20,6 +20,13 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "sections")] public IEnumerable Sections { get; set; } + /// + /// Gets the available sections (i.e. to populate a drop down) + /// The key is the Alias the value is the Name - the Alias is what is used in the AllowedSections property and for persistence + /// + [DataMember(Name = "availableSections")] + public IEnumerable
AvailableSections { get; set; } + [DataMember(Name = "startNodeContent")] public int StartContentId { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs index 8bef19d62f..8f42dbf169 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs @@ -30,11 +30,11 @@ namespace Umbraco.Web.Models.ContentEditing [Required] public IEnumerable UserGroups { get; set; } - [DataMember(Name = "startContentId")] - public int StartContentId { get; set; } + [DataMember(Name = "startContentIds")] + public int[] StartContentIds { get; set; } - [DataMember(Name = "startMediaId")] - public int StartMediaId { get; set; } + [DataMember(Name = "startMediaIds")] + public int[] StartMediaIds { get; set; } [DataMember(Name = "allowedSections")] public IEnumerable AllowedSections { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index aa4ad4b6e4..96d52f53fd 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -16,8 +16,29 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { - config.CreateMap() + config.CreateMap() .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) + .ForMember(user => user.Id, expression => expression.Ignore()) + .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) + .ForMember(user => user.StartContentIds, expression => expression.Ignore()) + .ForMember(user => user.StartMediaIds, expression => expression.Ignore()) + .ForMember(user => user.Language, expression => expression.Ignore()) + .ForMember(user => user.SecurityStamp, expression => expression.Ignore()) + .ForMember(user => user.ProviderUserKey, expression => expression.Ignore()) + .ForMember(user => user.Username, expression => expression.Ignore()) + .ForMember(user => user.RawPasswordValue, expression => expression.Ignore()) + .ForMember(user => user.PasswordQuestion, expression => expression.Ignore()) + .ForMember(user => user.RawPasswordAnswerValue, expression => expression.Ignore()) + .ForMember(user => user.Comments, expression => expression.Ignore()) + .ForMember(user => user.IsApproved, expression => expression.Ignore()) + .ForMember(user => user.IsLockedOut, expression => expression.Ignore()) + .ForMember(user => user.LastLoginDate, expression => expression.Ignore()) + .ForMember(user => user.LastPasswordChangeDate, expression => expression.Ignore()) + .ForMember(user => user.LastLockoutDate, expression => expression.Ignore()) + .ForMember(user => user.FailedPasswordAttempts, expression => expression.Ignore()) + .ForMember(user => user.DeletedDate, expression => expression.Ignore()) + .ForMember(user => user.CreateDate, expression => expression.Ignore()) + .ForMember(user => user.UpdateDate, expression => expression.Ignore()) .AfterMap((invite, user) => { foreach (var group in invite.UserGroups) @@ -27,8 +48,9 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() - .ForMember(detail => detail.Notifications, opt => opt.Ignore()) + .ForMember(detail => detail.AvailableSections, opt => opt.MapFrom(x => applicationContext.Services.SectionService.GetSections())) .ForMember(detail => detail.Sections, opt => opt.MapFrom(x => x.AllowedSections)) + .ForMember(detail => detail.Notifications, opt => opt.Ignore()) .ForMember(detail => detail.Udi, opt => opt.Ignore()) .ForMember(detail => detail.Trashed, opt => opt.Ignore()) .ForMember(detail => detail.ParentId, opt => opt.UseValue(-1)) @@ -37,15 +59,12 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) - .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) - .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds)) + .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember( detail => detail.AvailableUserGroups, - opt => opt.MapFrom(user => applicationContext.Services.UserService.GetAllUserGroups().ToDictionary(x => x.Alias, x => x.Name))) - .ForMember( - detail => detail.AvailableSections, - opt => opt.MapFrom(user => applicationContext.Services.SectionService.GetSections().ToDictionary(x => x.Alias, x => x.Name))) + opt => opt.MapFrom(user => applicationContext.Services.UserService.GetAllUserGroups())) .ForMember( detail => detail.AvailableCultures, opt => opt.MapFrom(user => applicationContext.Services.TextService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName))) @@ -66,8 +85,8 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => GetIntId(user.Id))) - .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) - .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds)) + .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember( detail => detail.EmailHash, @@ -76,8 +95,8 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => user.Id)) - .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) - .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds)) + .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture)) .ForMember(detail => detail.AllowedSections, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember( @@ -94,8 +113,8 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Groups.ToArray())) - .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) - .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.StartContentNodes, opt => opt.MapFrom(user => user.StartContentIds)) + .ForMember(detail => detail.StartMediaNodes, opt => opt.MapFrom(user => user.StartMediaIds)) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember(detail => detail.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp)); diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 05407a93e5..d610ed6945 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -243,8 +243,6 @@ namespace Umbraco.Web.Security Name = membershipUser.UserName, RawPasswordValue = Guid.NewGuid().ToString("N"), //Need to set this to something - will not be used though Username = membershipUser.UserName, - StartContentId = -1, - StartMediaId = -1, IsLockedOut = false, IsApproved = true }; diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index a9f0bda805..076c7b093a 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Trees { var node = base.CreateRootNode(queryStrings); //if the user's start node is not default, then ensure the root doesn't have a menu - if (Security.CurrentUser.StartContentId != Constants.System.Root) + if (Security.CurrentUser.StartContentIds.Length > 0 && Security.CurrentUser.StartContentIds.Contains(Constants.System.Root) == false) { node.MenuUrl = ""; } @@ -60,9 +60,9 @@ namespace Umbraco.Web.Trees get { return Services.ContentService.RecycleBinSmells(); } } - protected override int UserStartNode + protected override int[] UserStartNodes { - get { return Security.CurrentUser.StartContentId; } + get { return Security.CurrentUser.StartContentIds; } } /// @@ -121,7 +121,7 @@ namespace Umbraco.Web.Trees var menu = new MenuItemCollection(); //if the user's start node is not the root then ensure the root menu is empty/doesn't exist - if (Security.CurrentUser.StartContentId != Constants.System.Root) + if (Security.CurrentUser.StartContentIds.Length > 0 && Security.CurrentUser.StartContentIds.Contains(Constants.System.Root) == false) { return menu; } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 97c5daed96..f8e8bec293 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -68,7 +68,7 @@ namespace Umbraco.Web.Trees /// /// Returns the user's start node for this tree /// - protected abstract int UserStartNode { get; } + protected abstract int[] UserStartNodes { get; } /// /// Gets the tree nodes for the given id @@ -105,7 +105,7 @@ namespace Umbraco.Web.Trees // Therefore, in the latter case, we want to change the id to -1 since we want to render the current user's root node // and the GetChildEntities method will take care of rendering the correct root node. // If it is in dialog mode, then we don't need to change anything and the children will just render as per normal. - if (IsDialog(queryStrings) == false && UserStartNode != Constants.System.Root) + if (IsDialog(queryStrings) == false && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) { id = Constants.System.Root.ToString(CultureInfo.InvariantCulture); } @@ -139,17 +139,11 @@ namespace Umbraco.Web.Trees //if a request is made for the root node data but the user's start node is not the default, then - // we need to return their start node data - if (iid == Constants.System.Root && UserStartNode != Constants.System.Root) + // we need to return their start nodes + if (iid == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) { - //just return their single start node, it will show up under the 'Content' label - var startNode = Services.EntityService.Get(UserStartNode, UmbracoObjectType); - if (startNode != null) - return new[] { startNode }; - else - { - throw new EntityNotFoundException(UserStartNode, "User's start content node could not be found"); - } + var nodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes); + return nodes; } return Services.EntityService.GetChildren(iid, UmbracoObjectType).ToArray(); @@ -175,7 +169,7 @@ namespace Umbraco.Web.Trees protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { //check if we're rendering the root - if (id == Constants.System.Root.ToInvariantString() && UserStartNode == Constants.System.Root) + if (id == Constants.System.Root.ToInvariantString() && (UserStartNodes.Length == 0 || UserStartNodes.Contains(Constants.System.Root))) { var altStartId = string.Empty; diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index ed72857a40..068d6641b0 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Trees { var node = base.CreateRootNode(queryStrings); //if the user's start node is not default, then ensure the root doesn't have a menu - if (Security.CurrentUser.StartMediaId != Constants.System.Root) + if (Security.CurrentUser.StartMediaIds.Length > 0 && Security.CurrentUser.StartMediaIds.Contains(Constants.System.Root) == false) { node.MenuUrl = ""; } @@ -53,9 +53,9 @@ namespace Umbraco.Web.Trees get { return Services.MediaService.RecycleBinSmells(); } } - protected override int UserStartNode + protected override int[] UserStartNodes { - get { return Security.CurrentUser.StartMediaId; } + get { return Security.CurrentUser.StartMediaIds; } } /// @@ -100,7 +100,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.Root.ToInvariantString()) { //if the user's start node is not the root then ensure the root menu is empty/doesn't exist - if (Security.CurrentUser.StartMediaId != Constants.System.Root) + if (Security.CurrentUser.StartMediaIds.Length > 0 && Security.CurrentUser.StartMediaIds.Contains(Constants.System.Root) == false) { return menu; } diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs index 3feb3b682a..3ae70ac5e8 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -44,9 +44,9 @@ namespace Umbraco.Web.WebApi.Filters FilterBasedOnPermissions(items, user, ApplicationContext.Current.Services.UserService); } - protected override int GetUserStartNode(IUser user) + protected override int[] GetUserStartNodes(IUser user) { - return user.StartContentId; + return user.StartContentIds; } protected override int RecycleBinId diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs index 36170627d9..72cea5c2d0 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -39,9 +39,9 @@ namespace Umbraco.Web.WebApi.Filters get { return true; } } - protected virtual int GetUserStartNode(IUser user) + protected virtual int[] GetUserStartNodes(IUser user) { - return user.StartMediaId; + return user.StartMediaIds; } protected virtual int RecycleBinId @@ -85,8 +85,8 @@ namespace Umbraco.Web.WebApi.Filters var toRemove = new List(); foreach (dynamic item in items) { - var hasPathAccess = (item != null && UserExtensions.HasPathAccess(item.Path, GetUserStartNode(user), RecycleBinId)); - if (!hasPathAccess) + var hasPathAccess = (item != null && UserExtensions.HasPathAccess(item.Path, GetUserStartNodes(user), RecycleBinId)); + if (hasPathAccess == false) { toRemove.Add(item); } diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index a49c086be3..eb43857b45 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -294,8 +294,8 @@ namespace umbraco.BasePages RealName = u.Name, //currently we only have one user type! Roles = u.GetGroups(), - StartContentNode = u.StartNodeId, - StartMediaNode = u.StartMediaId, + StartContentNodes = u.UserEntity.StartContentIds, + StartMediaNodes = u.UserEntity.StartMediaIds, Username = u.LoginName, Culture = ui.Culture(u) diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 3a425ddef3..94d4802e48 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -707,39 +707,38 @@ namespace umbraco.BusinessLogic UserEntity.IsApproved = value == false; } } - - /// - /// - /// Gets or sets the start content node id. - /// - /// The start node id. + + [Obsolete("This should not be used, it will return invalid data because a user can have multiple start nodes, this will only return the first")] public int StartNodeId { get { if (_lazyId.HasValue) SetupUser(_lazyId.Value); - return UserEntity.StartContentId; + return UserEntity.StartContentIds == null ? -1 : UserEntity.StartContentIds[0]; } set { - UserEntity.StartContentId = value; + if (UserEntity.StartContentIds == null) + UserEntity.StartContentIds = new int[] {value}; + else if (UserEntity.StartContentIds.Contains(value) == false) + UserEntity.StartContentIds = UserEntity.StartContentIds.Concat(new[] {value}).ToArray(); } } - /// - /// Gets or sets the start media id. - /// - /// The start media id. + [Obsolete("This should not be used, it will return invalid data because a user can have multiple start nodes, this will only return the first")] public int StartMediaId { get { if (_lazyId.HasValue) SetupUser(_lazyId.Value); - return UserEntity.StartMediaId; + return UserEntity.StartMediaIds == null ? -1 : UserEntity.StartMediaIds[0]; } set { - UserEntity.StartMediaId = value; + if (UserEntity.StartMediaIds == null) + UserEntity.StartMediaIds = new int[] { value }; + else if (UserEntity.StartMediaIds.Contains(value) == false) + UserEntity.StartMediaIds = UserEntity.StartMediaIds.Concat(new[] { value }).ToArray(); } } From 59872b0e17c3fb79334e407b7321f4425ef25e3f Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 25 May 2017 02:11:38 +1000 Subject: [PATCH 095/714] fixes ability to create new users with the legacy editor --- src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml | 8 -------- src/Umbraco.Web.UI/umbraco/config/create/UI.xml | 2 +- src/umbraco.businesslogic/User.cs | 4 ++-- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml index 1cb5e32201..26aaddb78f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml @@ -43,14 +43,6 @@ - -
User
- /create/user.ascx - - - - -
User
/create/simple.ascx diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml index af2eece2ab..265c5b5642 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml @@ -43,7 +43,7 @@
- +
User
/create/user.ascx diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 94d4802e48..75fa3d903d 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -714,7 +714,7 @@ namespace umbraco.BusinessLogic get { if (_lazyId.HasValue) SetupUser(_lazyId.Value); - return UserEntity.StartContentIds == null ? -1 : UserEntity.StartContentIds[0]; + return UserEntity.StartContentIds == null || UserEntity.StartContentIds.Length == 0 ? -1 : UserEntity.StartContentIds[0]; } set { @@ -731,7 +731,7 @@ namespace umbraco.BusinessLogic get { if (_lazyId.HasValue) SetupUser(_lazyId.Value); - return UserEntity.StartMediaIds == null ? -1 : UserEntity.StartMediaIds[0]; + return UserEntity.StartMediaIds == null || UserEntity.StartMediaIds.Length == 0 ? -1 : UserEntity.StartMediaIds[0]; } set { From 2cd23cb24f0771bf2eda7a8a4b3d0e81e5151caa Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 25 May 2017 02:46:21 +1000 Subject: [PATCH 096/714] Updates business logic to persist the start nodes for users --- .../Models/Rdbms/UserStartNodeDto.cs | 2 +- .../Persistence/Factories/UserFactory.cs | 4 +- .../Repositories/UserRepository.cs | 51 ++++++++++++++++++- src/umbraco.businesslogic/User.cs | 10 +--- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs index e872111829..42f3e5a7ca 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("startNodeType")] [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode", Name = "IX_umbracoUserStartNode_startNodeType")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode, userId", Name = "IX_umbracoUserStartNode_startNodeType")] public int StartNodeType { get; set; } public enum StartNodeTypeValue diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index e7f2aba08b..c4154a7908 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -32,8 +32,8 @@ namespace Umbraco.Core.Persistence.Factories user.DisableChangeTracking(); user.Key = guidId; - user.StartContentIds = dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.Id).ToArray(); - user.StartMediaIds = dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int) UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.Id).ToArray(); + user.StartContentIds = dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.StartNode).ToArray(); + user.StartMediaIds = dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int) UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.StartNode).ToArray(); user.IsLockedOut = dto.NoConsole; user.IsApproved = dto.Disabled == false; user.Language = dto.UserLanguage; diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 4b05b5a47a..4f3fb1b6e7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -204,6 +204,18 @@ namespace Umbraco.Core.Persistence.Repositories var id = Convert.ToInt32(Database.Insert(userDto)); entity.Id = id; + if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) + { + if (entity.IsPropertyDirty("StartContentIds")) + { + AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); + } + if (entity.IsPropertyDirty("StartMediaIds")) + { + AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); + } + } + if (entity.IsPropertyDirty("Groups")) { //lookup all assigned @@ -228,7 +240,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistUpdatedItem(IUser entity) { //Updates Modified date - ((User)entity).UpdatingEntity(); + ((User)entity).UpdatingEntity(); //ensure security stamp if non if (entity.SecurityStamp.IsNullOrWhiteSpace()) @@ -287,8 +299,21 @@ namespace Umbraco.Core.Persistence.Repositories Database.Update(userDto, changedCols); } + if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) + { + var assignedStartNodes = Database.Fetch("SELECT * FROM umbracoUserStartNode WHERE userId = @userId", new { userId = entity.Id }); + if (entity.IsPropertyDirty("StartContentIds")) + { + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); + } + if (entity.IsPropertyDirty("StartMediaIds")) + { + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); + } + } + if (entity.IsPropertyDirty("Groups")) - { + { //lookup all assigned var assigned = entity.Groups == null || entity.Groups.Any() == false ? new List() @@ -312,6 +337,28 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } + private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, UserStartNodeDto.StartNodeTypeValue startNodeType, int[] entityStartIds) + { + var assignedIds = current.Where(x => x.StartNodeType == (int)startNodeType).Select(x => x.StartNode).ToArray(); + + //remove the ones not assigned to the entity + var toDelete = assignedIds.Except(entityStartIds).ToArray(); + if (toDelete.Length > 0) + Database.Delete("WHERE UserId = @UserId AND startNode IN (@startNodes)", new {UserId = entity.Id, startNodes = toDelete}); + //add the ones not currently in the db + var toAdd = entityStartIds.Except(assignedIds).ToArray(); + foreach (var i in toAdd) + { + var dto = new UserStartNodeDto + { + StartNode = i, + StartNodeType = (int)startNodeType, + UserId = entity.Id + }; + Database.Insert(dto); + } + } + #endregion #region Implementation of IUserRepository diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 75fa3d903d..114c77e340 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -718,10 +718,7 @@ namespace umbraco.BusinessLogic } set { - if (UserEntity.StartContentIds == null) - UserEntity.StartContentIds = new int[] {value}; - else if (UserEntity.StartContentIds.Contains(value) == false) - UserEntity.StartContentIds = UserEntity.StartContentIds.Concat(new[] {value}).ToArray(); + UserEntity.StartContentIds = new int[] { value }; } } @@ -735,10 +732,7 @@ namespace umbraco.BusinessLogic } set { - if (UserEntity.StartMediaIds == null) - UserEntity.StartMediaIds = new int[] { value }; - else if (UserEntity.StartMediaIds.Contains(value) == false) - UserEntity.StartMediaIds = UserEntity.StartMediaIds.Concat(new[] { value }).ToArray(); + UserEntity.StartMediaIds = new int[] { value }; } } From e9c917e60b1658b3b575a6a4f1bcfdecba435cfc Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 24 May 2017 22:35:47 +0200 Subject: [PATCH 097/714] add resource for getUserGroups --- .../src/common/resources/users.resource.js | 48 +++---------------- 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 35f85063e0..66a292d516 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -141,49 +141,13 @@ return deferred.promise; } - //TODO: Change this over to the real resource function getUserGroups() { - var deferred = $q.defer(); - var userGroups = [ - { - "name": "Admin", - "alias": "admin", - "id": 1, - "icon": "icon-medal", - "sections": [], - "startNodesContent": [], - "startNodesMedia": [] - }, - { - "name": "Writer", - "alias": "writer", - "id": 2, - "icon": "icon-edit", - "sections": [{ "id": "1", "name": "Content" }, { "id": "2", "name": "Media" }], - "startNodesContent": [], - "startNodesMedia": [] - }, - { - "name": "Editor", - "alias": "editor", - "id": 3, - "icon": "icon-tools", - "sections": [{ "id": "1", "name": "Content" }, { "id": "2", "name": "Media" }], - "startNodesContent": [{ "id": "1", "name": "Start node 1" }, { "id": "2", "name": "Start node 2" }], - "startNodesMedia": [{ "id": "1", "name": "Start node 1" }, { "id": "2", "name": "Start node 2" }] - }, - { - "name": "Translator", - "alias": "translator", - "id": 4, - "icon": "icon-globe", - "sections": [{ "id": "1", "name": "Content" }, { "id": "2", "name": "Translations" }], - "startNodesContent": [{ "id": "1", "name": "Start node 1" }], - "startNodesMedia": [{ "id": "1", "name": "Start node 1" }] - } - ]; - deferred.resolve(userGroups); - return deferred.promise; + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "GetUserGroups")), + "Failed to retrieve user groups"); } var resource = { From b95ba2e9a7e4b3264ad73962347c9c7bc767f3f1 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 24 May 2017 22:36:44 +0200 Subject: [PATCH 098/714] wire up user group picker --- .../common/overlays/usergrouppicker/usergrouppicker.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/usergrouppicker/usergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/usergrouppicker/usergrouppicker.html index 9f239a11f0..110465101b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/usergrouppicker/usergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/usergrouppicker/usergrouppicker.html @@ -40,7 +40,7 @@ Content start node: {{ startNode.name }}, - Content root + Content root @@ -48,7 +48,7 @@ Media start node: {{ startNode.name }}, - Media root + Media root From 939d1286627fb43bab00bfeab67a3e3fca79d8e6 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 24 May 2017 22:43:51 +0200 Subject: [PATCH 099/714] fix load indicator on user --- .../src/views/users/user.controller.js | 7 +------ src/Umbraco.Web.UI.Client/src/views/users/user.html | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index ecb0eced69..c559994fb9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -5,7 +5,6 @@ var vm = this; - vm.loading = false; vm.page = {}; vm.user = {}; vm.breadcrumbs = []; @@ -28,12 +27,8 @@ usersResource.getUser($routeParams.id).then(function (user) { vm.user = user; makeBreadcrumbs(vm.user); - }); - - // fake loading - $timeout(function () { vm.loading = false; - }, 500); + }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 32c4ea14c0..a703a45139 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -15,7 +15,7 @@ -
+
From 911fdf20a4ededcaad31abc675356f86adaec985 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 24 May 2017 22:53:25 +0200 Subject: [PATCH 100/714] add watcher to avatar name --- .../common/directives/components/umbavatar.directive.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js index 51c8d660db..5c2fdb594c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js @@ -53,7 +53,8 @@ Use this directive to render an avatar. function AvatarDirective() { function link(scope, element, attrs, ctrl) { - + + var eventBindings = []; scope.initials = ""; function onInit() { @@ -68,6 +69,12 @@ Use this directive to render an avatar. } } + eventBindings.push(scope.$watch('name', function (newValue, oldValue) { + if (newValue === oldValue) { return; } + if (oldValue === undefined || newValue === undefined) { return; } + scope.initials = getNameInitials(newValue); + })); + onInit(); } From cb097f11a18f65de6b657b2b75e6c026c26e9719 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 25 May 2017 00:13:11 +0200 Subject: [PATCH 101/714] add save user and create user --- .../src/common/resources/users.resource.js | 37 +- .../src/views/users/user.controller.js | 31 +- .../users/views/users/users.controller.js | 34 +- .../src/views/users/views/users/users.html | 646 +++++++++--------- 4 files changed, 421 insertions(+), 327 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 66a292d516..158640cff0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -100,7 +100,34 @@ "GetById", { id: userId })), "Failed to retrieve data for user " + userId); + } + function createUser(user) { + if (!user) { + throw "user not specified"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostCreateUser"), + user), + "Failed to save user"); + } + + function saveUser(user) { + if (!user) { + throw "user not specified"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostSaveUser"), + user), + "Failed to save user"); } function getUserGroup() { @@ -151,12 +178,14 @@ } var resource = { - getUser: getUser, - getUserGroup: getUserGroup, - getUserGroups: getUserGroups, disableUsers: disableUsers, enableUsers: enableUsers, - getPagedResults: getPagedResults + getPagedResults: getPagedResults, + getUser: getUser, + createUser: createUser, + saveUser: saveUser, + getUserGroup: getUserGroup, + getUserGroups: getUserGroups }; return resource; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index c559994fb9..c69fe3ad6e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,9 +1,10 @@ (function () { "use strict"; - function UserEditController($scope, $timeout, $location, usersResource, $routeParams) { + function UserEditController($scope, $timeout, $location, $routeParams, usersResource, contentEditingHelper, localizationService, notificationsService) { var vm = this; + var localizeSaving = localizationService.localize("general_saving"); vm.page = {}; vm.user = {}; @@ -18,6 +19,7 @@ vm.resetPassword = resetPassword; vm.getUserStateType = getUserStateType; vm.changeAvatar = changeAvatar; + vm.save = save; function init() { @@ -32,6 +34,33 @@ } + function save() { + + vm.page.saveButtonState = "busy"; + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: usersResource.saveUser, + scope: $scope, + content: vm.user, + // We do not redirect on failure for users - this is because it is not possible to actually save a user + // when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) {} + }).then(function (saved) { + + vm.user = saved; + vm.page.saveButtonState = "success"; + + }, function (err) { + + vm.page.saveButtonState = "error"; + + }); + + } + function goToPage(ancestor) { $location.path(ancestor.path).search("subview", ancestor.subView); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 10db1fb908..81cb3737e5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -1,9 +1,10 @@ (function () { "use strict"; - function UsersController($scope, $timeout, $location, usersResource) { + function UsersController($scope, $timeout, $location, usersResource, localizationService, contentEditingHelper) { var vm = this; + var localizeSaving = localizationService.localize("general_saving"); vm.users = []; vm.userGroups = []; @@ -69,6 +70,7 @@ vm.areAllSelected = areAllSelected; vm.searchUsers = searchUsers; vm.setOrderByFilter = setOrderByFilter; + vm.createUser = createUser; function init() { @@ -206,6 +208,34 @@ getUsers(); } + function createUser() { + + vm.newUser.id = -1; + vm.newUser.parentId = -1; + vm.page.createButtonState = "busy"; + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: usersResource.createUser, + scope: $scope, + content: vm.newUser, + // We do not redirect on failure for users - this is because it is not possible to actually save a user + // when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) {} + }).then(function (saved) { + + vm.page.createButtonState = "success"; + + }, function (err) { + + vm.page.createButtonState = "error"; + + }); + + } + // helpers function getUsers() { @@ -225,7 +255,7 @@ formatDates(vm.users); vm.loading = false; - + }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index e4b9532ec0..3571719d8e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -1,353 +1,359 @@
- +
- -
- - + + + +
- - - - - + + + + + + + - - - - - - -
/// public IEnumerable GetUserGroups() - { - return Mapper.Map< IEnumerable, IEnumerable>(Services.UserService.GetAllUserGroups()); + { + return Mapper.Map, IEnumerable>(Services.UserService.GetAllUserGroups()); } /// @@ -111,7 +122,7 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - + var existing = Services.UserService.GetByEmail(userSave.Email); if (existing != null) { @@ -177,7 +188,7 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - + var intId = userSave.Id.TryConvertTo(); if (intId.Success == false) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -189,28 +200,32 @@ namespace Umbraco.Web.Editors var hasErrors = false; var existing = Services.UserService.GetByEmail(userSave.Email); - if (existing != null && existing.Id != (int)userSave.Id) + if (existing != null && existing.Id != userSave.Id) { ModelState.AddModelError("Email", "A user with the email already exists"); hasErrors = true; } existing = Services.UserService.GetByUsername(userSave.Name); - if (existing != null && existing.Id != (int)userSave.Id) + if (existing != null && existing.Id != userSave.Id) { ModelState.AddModelError("Email", "A user with the email already exists"); hasErrors = true; - } + } if (hasErrors) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); //TODO: More validation, password changing logic, persisting - var user = Mapper.Map(userSave); + //merge the save data onto the user + var user = Mapper.Map(userSave, found); Services.UserService.Save(user); - return Mapper.Map(user); + var display = Mapper.Map(user); + + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/operationSavedHeader"), Services.TextService.Localize("speechBubbles/editUserSaved")); + return display; } /// @@ -222,7 +237,7 @@ namespace Umbraco.Web.Editors var users = Services.UserService.GetUsersById(userIds).ToArray(); foreach (var u in users) { - u.IsApproved = false; + u.IsApproved = false; } Services.UserService.Save(users); @@ -238,7 +253,7 @@ namespace Umbraco.Web.Editors var users = Services.UserService.GetUsersById(userIds).ToArray(); foreach (var u in users) { - u.IsApproved = true; + u.IsApproved = true; } Services.UserService.Save(users); diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index e4bbe7850b..29b95e75eb 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -19,6 +19,9 @@ namespace Umbraco.Web.Models.ContentEditing Notifications = new List(); } + [DataMember(Name = "username")] + public string Username { get; set; } + /// /// The MD5 lowercase hash of the email which can be used by gravatar /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs index 8f42dbf169..8e7b247b23 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs @@ -17,7 +17,15 @@ namespace Umbraco.Web.Models.ContentEditing { //TODO: There will be more information to save along with the structure for changing passwords - [DataMember(Name = "locale", IsRequired = true)] + [DataMember(Name = "id", IsRequired = true)] + [Required] + public new int Id { get; set; } + + [DataMember(Name = "username", IsRequired = true)] + [Required] + public string Username { get; set; } + + [DataMember(Name = "culture", IsRequired = true)] [Required] public string Culture { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 96d52f53fd..731d9037f3 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -15,7 +15,35 @@ namespace Umbraco.Web.Models.Mapping internal class UserModelMapper : MapperConfiguration { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) - { + { + //Used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! + config.CreateMap() + .ForMember(user => user.Language, expression => expression.MapFrom(save => save.Culture)) + .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) + .ForMember(user => user.SecurityStamp, expression => expression.Ignore()) + .ForMember(user => user.ProviderUserKey, expression => expression.Ignore()) + .ForMember(user => user.RawPasswordValue, expression => expression.Ignore()) + .ForMember(user => user.PasswordQuestion, expression => expression.Ignore()) + .ForMember(user => user.RawPasswordAnswerValue, expression => expression.Ignore()) + .ForMember(user => user.Comments, expression => expression.Ignore()) + .ForMember(user => user.IsApproved, expression => expression.Ignore()) + .ForMember(user => user.IsLockedOut, expression => expression.Ignore()) + .ForMember(user => user.LastLoginDate, expression => expression.Ignore()) + .ForMember(user => user.LastPasswordChangeDate, expression => expression.Ignore()) + .ForMember(user => user.LastLockoutDate, expression => expression.Ignore()) + .ForMember(user => user.FailedPasswordAttempts, expression => expression.Ignore()) + .ForMember(user => user.DeletedDate, expression => expression.Ignore()) + .ForMember(user => user.CreateDate, expression => expression.Ignore()) + .ForMember(user => user.UpdateDate, expression => expression.Ignore()) + .AfterMap((save, user) => + { + user.ClearGroups(); + foreach (var group in save.UserGroups) + { + user.AddGroup(group); + } + }); + config.CreateMap() .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) .ForMember(user => user.Id, expression => expression.Ignore()) @@ -57,7 +85,8 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.Path, opt => opt.MapFrom(user => "-1," + user.Id)) .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()); - config.CreateMap() + config.CreateMap() + .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds)) .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds)) diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 114c77e340..54748f7637 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -648,10 +648,7 @@ namespace umbraco.BusinessLogic public void ClearGroups() { if (_lazyId.HasValue) SetupUser(_lazyId.Value); - foreach (var group in UserEntity.Groups.ToArray()) - { - UserEntity.RemoveGroup(group); - } + UserEntity.ClearGroups(); } /// From 528be48437e5e4272adcb64159ef5cacaf458350 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 May 2017 00:02:32 +1000 Subject: [PATCH 103/714] Adds UsersController.SetAvatar and ClearAvatar --- src/Umbraco.Core/Models/Membership/IUser.cs | 5 + src/Umbraco.Core/Models/Membership/User.cs | 9 + src/Umbraco.Core/Models/Rdbms/UserDto.cs | 7 + .../Persistence/Factories/UserFactory.cs | 4 +- .../src/common/resources/users.resource.js | 9 +- .../src/common/services/util.service.js | 165 +++++++++--------- src/Umbraco.Web/Editors/UsersController.cs | 83 ++++++++- .../Models/ContentEditing/UserSave.cs | 5 +- .../Models/Mapping/UserModelMapper.cs | 2 + 9 files changed, 201 insertions(+), 88 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index c679181963..d6133c04dc 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -37,5 +37,10 @@ namespace Umbraco.Core.Models.Membership /// 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 + /// + string Avatar { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 9c444b48e4..e3c4d910af 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -95,6 +95,7 @@ namespace Umbraco.Core.Models.Membership private string _name; private string _securityStamp; + private string _avatar; private int _sessionTimeout; private int[] _startContentIds; private int[] _startMediaIds; @@ -124,6 +125,7 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo LastPasswordChangeDateSelector = ExpressionHelper.GetPropertyInfo(x => x.LastPasswordChangeDate); public readonly PropertyInfo SecurityStampSelector = ExpressionHelper.GetPropertyInfo(x => x.SecurityStamp); + public readonly PropertyInfo AvatarSelector = ExpressionHelper.GetPropertyInfo(x => x.Avatar); public readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentIds); public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaIds); @@ -272,6 +274,13 @@ namespace Umbraco.Core.Models.Membership set { SetPropertyValueAndDetectChanges(value, ref _securityStamp, Ps.Value.SecurityStampSelector); } } + [DataMember] + public string Avatar + { + get { return _avatar; } + set { SetPropertyValueAndDetectChanges(value, ref _avatar, Ps.Value.AvatarSelector); } + } + /// /// Gets or sets the session timeout. /// diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 0f74a92b7b..98dee12a94 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -79,6 +79,13 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.NotNull)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Avatar { get; set; } [ResultColumn] public List UserGroupDtos { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index c4154a7908..64a63447d9 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -44,6 +44,7 @@ namespace Umbraco.Core.Persistence.Factories user.LastPasswordChangeDate = dto.LastPasswordChangeDate ?? DateTime.MinValue; user.CreateDate = dto.CreateDate; user.UpdateDate = dto.UpdateDate; + user.Avatar = dto.Avatar; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -74,7 +75,8 @@ namespace Umbraco.Core.Persistence.Factories LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? (DateTime?)null : entity.LastLoginDate, LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate + UpdateDate = entity.UpdateDate, + Avatar = entity.Avatar }; foreach (var startNodeId in entity.StartContentIds) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 158640cff0..c70729246f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -9,7 +9,7 @@ (function () { 'use strict'; - function usersResource($http, umbRequestHelper, $q) { + function usersResource($http, umbRequestHelper, $q, umbDataFormatter) { function disableUsers(userIds) { if (!userIds) { @@ -121,15 +121,18 @@ throw "user not specified"; } + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserPostData(user); + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "userApiBaseUrl", "PostSaveUser"), - user), + formattedSaveData), "Failed to save user"); } - + function getUserGroup() { var deferred = $q.defer(); var user = { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index ba395becfc..f093d4bb31 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -4,7 +4,7 @@ function versionHelper() { return { //see: https://gist.github.com/TheDistantSea/8021359 - versionCompare: function(v1, v2, options) { + versionCompare: function (v1, v2, options) { var lexicographical = options && options.lexicographical, zeroExtend = options && options.zeroExtend, v1parts = v1.split('.'), @@ -61,15 +61,15 @@ angular.module('umbraco.services').factory('versionHelper', versionHelper); function dateHelper() { return { - - convertToServerStringTime: function(momentLocal, serverOffsetMinutes, format) { + + convertToServerStringTime: function (momentLocal, serverOffsetMinutes, format) { //get the formatted offset time in HH:mm (server time offset is in minutes) var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + moment() - .startOf('day') - .minutes(Math.abs(serverOffsetMinutes)) - .format('HH:mm'); + .startOf('day') + .minutes(Math.abs(serverOffsetMinutes)) + .format('HH:mm'); var server = moment.utc(momentLocal).utcOffset(formattedOffset); return server.format(format ? format : "YYYY-MM-DD HH:mm:ss"); @@ -80,9 +80,9 @@ function dateHelper() { //get the formatted offset time in HH:mm (server time offset is in minutes) var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + moment() - .startOf('day') - .minutes(Math.abs(serverOffsetMinutes)) - .format('HH:mm'); + .startOf('day') + .minutes(Math.abs(serverOffsetMinutes)) + .format('HH:mm'); //convert to the iso string format var isoFormat = moment(strVal).format("YYYY-MM-DDTHH:mm:ss") + formattedOffset; @@ -121,30 +121,30 @@ angular.module('umbraco.services').factory('packageHelper', packageHelper); function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) { return { /** sets the image's url, thumbnail and if its a folder */ - setImageData: function(img) { - + setImageData: function (img) { + img.isFolder = !mediaHelper.hasFilePropertyType(img); - if(!img.isFolder){ + if (!img.isFolder) { img.thumbnail = mediaHelper.resolveFile(img, true); - img.image = mediaHelper.resolveFile(img, false); + img.image = mediaHelper.resolveFile(img, false); } }, /** sets the images original size properties - will check if it is a folder and if so will just make it square */ - setOriginalSize: function(img, maxHeight) { + setOriginalSize: function (img, maxHeight) { //set to a square by default img.originalWidth = maxHeight; img.originalHeight = maxHeight; - var widthProp = _.find(img.properties, function(v) { return (v.alias === "umbracoWidth"); }); + var widthProp = _.find(img.properties, function (v) { return (v.alias === "umbracoWidth"); }); if (widthProp && widthProp.value) { img.originalWidth = parseInt(widthProp.value, 10); if (isNaN(img.originalWidth)) { img.originalWidth = maxHeight; } } - var heightProp = _.find(img.properties, function(v) { return (v.alias === "umbracoHeight"); }); + var heightProp = _.find(img.properties, function (v) { return (v.alias === "umbracoHeight"); }); if (heightProp && heightProp.value) { img.originalHeight = parseInt(heightProp.value, 10); if (isNaN(img.originalHeight)) { @@ -154,7 +154,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, /** sets the image style which get's used in the angular markup */ - setImageStyle: function(img, width, height, rightMargin, bottomMargin) { + setImageStyle: function (img, width, height, rightMargin, bottomMargin) { img.style = { width: width + "px", height: height + "px", "margin-right": rightMargin + "px", "margin-bottom": bottomMargin + "px" }; img.thumbStyle = { "background-image": "url('" + img.thumbnail + "')", @@ -162,10 +162,10 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me "background-position": "center", "background-size": Math.min(width, img.originalWidth) + "px " + Math.min(height, img.originalHeight) + "px" }; - }, + }, /** gets the image's scaled wdith based on the max row height */ - getScaledWidth: function(img, maxHeight) { + getScaledWidth: function (img, maxHeight) { var scaled = img.originalWidth * maxHeight / img.originalHeight; return scaled; //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row @@ -173,7 +173,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, /** returns the target row width taking into account how many images will be in the row and removing what the margin is */ - getTargetWidth: function(imgsPerRow, maxRowWidth, margin) { + getTargetWidth: function (imgsPerRow, maxRowWidth, margin) { //take into account the margin, we will have 1 less margin item than we have total images return (maxRowWidth - ((imgsPerRow - 1) * margin)); }, @@ -187,7 +187,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me targetHeight = optional; */ - getRowHeightForImages: function(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { + getRowHeightForImages: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { var idealImages = imgs.slice(0, idealImgPerRow); //get the target row width without margin @@ -195,8 +195,8 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me //this gets the image with the smallest height which equals the maximum we can scale up for this image block var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight); //if the max scale height is smaller than the min display height, we'll use the min display height - targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); - + targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); + var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); if (attemptedRowHeight != null) { @@ -206,12 +206,12 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me if (attemptedRowHeight < minDisplayHeight) { if (idealImages.length > 1) { - + //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight); } - else { + else { //this will occur when we only have one image remaining in the row but it's still going to be too wide even when // using the minimum display height specified. In this case we're going to have to just crop the image in it's center // using the minimum display height and the full row width @@ -241,7 +241,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me //if we're already dealing with the ideal images per row and it's not quite wide enough, we can scale up a little bit so // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit) - + while (targetHeight < maxRowHeight && (maxRowHeight - targetHeight) > 5) { targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); @@ -273,7 +273,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, - performGetRowHeight: function(idealImages, targetRowWidth, minDisplayHeight, targetHeight) { + performGetRowHeight: function (idealImages, targetRowWidth, minDisplayHeight, targetHeight) { var currRowWidth = 0; @@ -285,7 +285,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me if (currRowWidth > targetRowWidth) { //get the new scaled height to fit var newHeight = targetRowWidth * targetHeight / currRowWidth; - + return newHeight; } else if (idealImages.length === 1 && (currRowWidth <= targetRowWidth) && !idealImages[0].isFolder) { @@ -303,7 +303,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, /** builds an image grid row */ - buildRow: function(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { + buildRow: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { var currRowWidth = 0; var row = { images: [] }; @@ -315,11 +315,11 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me for (var i = 0; i < imageRowHeight.imgCount; i++) { //get the lower width to ensure it always fits var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height)); - + if (currRowWidth + scaledWidth <= targetWidth) { - currRowWidth += scaledWidth; + currRowWidth += scaledWidth; sizes.push({ - width:scaledWidth, + width: scaledWidth, //ensure that the height is rounded height: Math.round(imageRowHeight.height) }); @@ -352,17 +352,17 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me if (row.images.length === 1 && totalRemaining > 1) { //if there's only one image on the row and there are more images remaining, set the container to max width - row.images[0].style.width = maxRowWidth + "px"; + row.images[0].style.width = maxRowWidth + "px"; } - + return row; }, /** Returns the maximum image scaling height for the current image collection */ - getMaxScaleableHeight: function(imgs, maxRowHeight) { + getMaxScaleableHeight: function (imgs, maxRowHeight) { - var smallestHeight = _.min(imgs, function(item) { return item.originalHeight; }).originalHeight; + var smallestHeight = _.min(imgs, function (item) { return item.originalHeight; }).originalHeight; //adjust the smallestHeight if it is larger than the static max row height if (smallestHeight > maxRowHeight) { @@ -372,10 +372,10 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */ - buildGrid: function(images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin,imagesOnly) { + buildGrid: function (images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin, imagesOnly) { var rows = []; - var imagesProcessed = 0; + var imagesProcessed = 0; //first fill in all of the original image sizes and URLs for (var i = startingIndex; i < images.length; i++) { @@ -384,7 +384,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me this.setImageData(item); this.setOriginalSize(item, maxRowHeight); - if(imagesOnly && !item.isFolder && !item.thumbnail){ + if (imagesOnly && !item.isFolder && !item.thumbnail) { images.splice(i, 1); i--; } @@ -449,7 +449,7 @@ function umbModelMapper() { /** This converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model */ convertToEntityBasic: function (source) { - var required = ["id", "name", "icon", "parentId", "path"]; + var required = ["id", "name", "icon", "parentId", "path"]; _.each(required, function (k) { if (!_.has(source, k)) { throw "The source object does not contain the property " + k; @@ -485,11 +485,11 @@ function umbSessionStorage($window) { get: function (key) { return angular.fromJson(storage["umb_" + key]); }, - - set : function(key, value) { + + set: function (key, value) { storage["umb_" + key] = angular.toJson(value); } - + }; } angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorage); @@ -504,26 +504,26 @@ angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorag */ function updateChecker($http, umbRequestHelper) { return { - - /** - * @ngdoc function - * @name umbraco.services.updateChecker#check - * @methodOf umbraco.services.updateChecker - * @function - * - * @description - * Called to load in the legacy tree js which is required on startup if a user is logged in or - * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. - */ - check: function() { - + + /** + * @ngdoc function + * @name umbraco.services.updateChecker#check + * @methodOf umbraco.services.updateChecker + * @function + * + * @description + * Called to load in the legacy tree js which is required on startup if a user is logged in or + * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. + */ + check: function () { + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "updateCheckApiBaseUrl", - "GetCheck")), - 'Failed to retrieve update status'); - } + $http.get( + umbRequestHelper.getApiUrl( + "updateCheckApiBaseUrl", + "GetCheck")), + 'Failed to retrieve update status'); + } }; } angular.module('umbraco.services').factory('updateChecker', updateChecker); @@ -546,7 +546,7 @@ function umbPropEditorHelper() { * * @param {string} input the view path currently stored for the property editor */ - getViewPath: function(input, isPreValue) { + getViewPath: function (input, isPreValue) { var path = String(input); if (path.startsWith('/')) { @@ -582,7 +582,7 @@ angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorH **/ function umbDataFormatter() { return { - + formatContentTypePostData: function (displayModel, action) { //create the save model from the display model @@ -594,7 +594,7 @@ function umbDataFormatter() { //TODO: Map these saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; }); saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null; - var realGroups = _.reject(displayModel.groups, function(g) { + var realGroups = _.reject(displayModel.groups, function (g) { //do not include these tabs return g.tabState === "init"; }); @@ -621,9 +621,9 @@ function umbDataFormatter() { return saveGroup; }); - + //we don't want any null groups - saveModel.groups = _.reject(saveModel.groups, function(g) { + saveModel.groups = _.reject(saveModel.groups, function (g) { return !g; }); @@ -631,7 +631,7 @@ function umbDataFormatter() { }, /** formats the display model used to display the data type to the model used to save the data type */ - formatDataTypePostData: function(displayModel, preValues, action) { + formatDataTypePostData: function (displayModel, preValues, action) { var saveModel = { parentId: displayModel.parentId, id: displayModel.id, @@ -651,14 +651,23 @@ function umbDataFormatter() { return saveModel; }, + /** formats the display model used to display the user to the model used to save the user */ + formatUserPostData: function (displayModel, preValues, action) { + + //create the save model from the display model + var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups'); + + return saveModel; + }, + /** formats the display model used to display the member to the model used to save the member */ - formatMemberPostData: function(displayModel, action) { + formatMemberPostData: function (displayModel, action) { //this is basically the same as for media but we need to explicitly add the username,email, password to the save model var saveModel = this.formatMediaPostData(displayModel, action); saveModel.key = displayModel.key; - + var genericTab = _.find(displayModel.tabs, function (item) { return item.id === 0; }); @@ -679,7 +688,7 @@ function umbDataFormatter() { saveModel.email = propEmail.value; saveModel.username = propLogin.value; saveModel.password = propPass.value; - + var selectedGroups = []; for (var n in propGroups.value) { if (propGroups.value[n] === true) { @@ -687,12 +696,12 @@ function umbDataFormatter() { } } saveModel.memberGroups = selectedGroups; - + //turn the dictionary into an array of pairs var memberProviderPropAliases = _.pairs(displayModel.fieldConfig); _.each(displayModel.tabs, function (tab) { _.each(tab.properties, function (prop) { - var foundAlias = _.find(memberProviderPropAliases, function(item) { + var foundAlias = _.find(memberProviderPropAliases, function (item) { return prop.alias === item[1]; }); if (foundAlias) { @@ -709,7 +718,7 @@ function umbDataFormatter() { saveModel.comments = prop.value; break; } - } + } }); }); @@ -719,7 +728,7 @@ function umbDataFormatter() { }, /** formats the display model used to display the media to the model used to save the media */ - formatMediaPostData: function(displayModel, action) { + formatMediaPostData: function (displayModel, action) { //NOTE: the display model inherits from the save model so we can in theory just post up the display model but // we don't want to post all of the data as it is unecessary. var saveModel = { @@ -744,7 +753,7 @@ function umbDataFormatter() { value: prop.value }); } - + }); }); @@ -760,8 +769,8 @@ function umbDataFormatter() { var genericTab = _.find(displayModel.tabs, function (item) { return item.id === 0; }); - - var propExpireDate = _.find(genericTab.properties, function(item) { + + var propExpireDate = _.find(genericTab.properties, function (item) { return item.alias === "_umb_expiredate"; }); var propReleaseDate = _.find(genericTab.properties, function (item) { diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 0e74de8af1..9bb136b31c 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Threading.Tasks; using System.Web; using System.Web.Http; using AutoMapper; using ClientDependency.Core; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -42,13 +45,89 @@ namespace Umbraco.Web.Editors { } - public IHttpActionResult SetAvatar(int id) + [FileUploadCleanupFilter(false)] + public async Task SetAvatar(int id) { - return Ok(); + if (Request.Content.IsMimeMultipartContent() == false) + { + throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); + } + + var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); + //ensure it exists + Directory.CreateDirectory(root); + var provider = new MultipartFormDataStreamProvider(root); + + var result = await Request.Content.ReadAsMultipartAsync(provider); + + //must have a file + if (result.FileData.Count == 0) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //get the string json from the request + var userId = result.FormData["userId"]; + int intUserId; + if (int.TryParse(userId, out intUserId) == false) + return Request.CreateValidationErrorResponse("The request was not formatted correctly, the userId is not an integer"); + + var user = Services.UserService.GetUserById(intUserId); + if (user == null) + return Request.CreateResponse(HttpStatusCode.NotFound); + + var tempFiles = new PostedFiles(); + + if (result.FileData.Count > 1) + return Request.CreateValidationErrorResponse("The request was not formatted correctly, only one file can be attached to the request"); + + //get the file info + var file = result.FileData[0]; + var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }).TrimEnd(); + var safeFileName = fileName.ToSafeFileName(); + var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + + if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) + { + if (user.Avatar.IsNullOrWhiteSpace()) + { + //we'll need to generate a new path! + //make it a hash of known data, we don't want this path to be guessable + user.Avatar = "UserAvatars/" + (user.Id + user.CreateDate.ToString("yyyyMMdd")).ToSHA1() + ext; + } + + using (var fs = System.IO.File.OpenRead(file.LocalFileName)) + { + FileSystemProviderManager.Current.MediaFileSystem.AddFile(user.Avatar, fs, true); + } + + Services.UserService.Save(user); + + //track the temp file so the cleanup filter removes it + tempFiles.UploadedFiles.Add(new ContentItemFile + { + TempFilePath = file.LocalFileName + }); + } + + return Request.CreateResponse(HttpStatusCode.OK, tempFiles); } public IHttpActionResult ClearAvatar(int id) { + var found = Services.UserService.GetUserById(id); + if (found == null) + return NotFound(); + + var filePath = found.Avatar; + + found.Avatar = null; + + Services.UserService.Save(found); + + if (FileSystemProviderManager.Current.MediaFileSystem.FileExists(filePath)) + FileSystemProviderManager.Current.MediaFileSystem.DeleteFile(filePath); + return Ok(); } diff --git a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs index 8e7b247b23..4b408d324e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs @@ -42,10 +42,7 @@ namespace Umbraco.Web.Models.ContentEditing public int[] StartContentIds { get; set; } [DataMember(Name = "startMediaIds")] - public int[] StartMediaIds { get; set; } - - [DataMember(Name = "allowedSections")] - public IEnumerable AllowedSections { get; set; } + public int[] StartMediaIds { get; set; } public IEnumerable Validate(ValidationContext validationContext) { diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 731d9037f3..aee0ae8336 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -19,6 +19,7 @@ namespace Umbraco.Web.Models.Mapping //Used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! config.CreateMap() .ForMember(user => user.Language, expression => expression.MapFrom(save => save.Culture)) + .ForMember(user => user.Avatar, expression => expression.Ignore()) .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) .ForMember(user => user.SecurityStamp, expression => expression.Ignore()) .ForMember(user => user.ProviderUserKey, expression => expression.Ignore()) @@ -47,6 +48,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) .ForMember(user => user.Id, expression => expression.Ignore()) + .ForMember(user => user.Avatar, expression => expression.Ignore()) .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) .ForMember(user => user.StartContentIds, expression => expression.Ignore()) .ForMember(user => user.StartMediaIds, expression => expression.Ignore()) From 8aabb3e3d004ff3eb9dd6db87b03c40b2c8ba41d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 May 2017 02:15:37 +1000 Subject: [PATCH 104/714] wires up custom user avatars --- src/Umbraco.Core/Models/UserExtensions.cs | 66 +++++++++++++ .../Repositories/UserRepository.cs | 3 +- .../src/common/resources/users.resource.js | 14 ++- .../src/controllers/main.controller.js | 41 +++----- .../views/install-local.controller.js | 2 +- .../src/views/users/user.controller.js | 97 +++++++++++++++---- .../src/views/users/user.html | 30 ++++-- .../src/views/users/views/users/users.html | 4 +- .../Editors/BackOfficeController.cs | 6 +- src/Umbraco.Web/Editors/GravatarController.cs | 38 -------- src/Umbraco.Web/Editors/UsersController.cs | 43 ++++---- .../Models/ContentEditing/UserDetail.cs | 6 ++ .../Models/ContentEditing/UserDisplay.cs | 7 +- .../Models/Mapping/UserModelMapper.cs | 5 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 - 15 files changed, 237 insertions(+), 126 deletions(-) delete mode 100644 src/Umbraco.Web/Editors/GravatarController.cs diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 4894be7b87..6636259c92 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,6 +1,9 @@ using System; using System.Globalization; using System.Linq; +using System.Net; +using Umbraco.Core.Cache; +using Umbraco.Core.IO; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -8,6 +11,69 @@ namespace Umbraco.Core.Models { public static class UserExtensions { + /// + /// Tries to lookup the user's gravatar to see if the endpoint can be reached, if so it returns the valid URL + /// + /// + /// + /// + /// + /// A list of 5 different sized avatar URLs + /// + internal static string[] GetCurrentUserAvatarUrls(this IUser user, IUserService userService, ICacheProvider staticCache) + { + if (user.Avatar.IsNullOrWhiteSpace()) + { + var gravatarHash = user.Email.ToMd5(); + var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash; + + //try gravatar + var gravatarAccess = staticCache.GetCacheItem("UserAvatar" + user.Id, () => + { + // Test if we can reach this URL, will fail when there's network or firewall errors + var request = (HttpWebRequest)WebRequest.Create(gravatarUrl); + // Require response within 10 seconds + request.Timeout = 10000; + try + { + using ((HttpWebResponse)request.GetResponse()) { } + } + catch (Exception) + { + // There was an HTTP or other error, return an null instead + return false; + } + return true; + }); + + if (gravatarAccess) + { + return new[] + { + gravatarUrl + "?s=30&d=mm", + gravatarUrl + "?s=60&d=mm", + gravatarUrl + "?s=90&d=mm", + gravatarUrl + "?s=150&d=mm", + gravatarUrl + "?s=300&d=mm" + }; + } + + return null; + } + + //use the custom avatar + var avatarUrl = FileSystemProviderManager.Current.MediaFileSystem.GetUrl(user.Avatar); + return new[] + { + avatarUrl + "?width=30&height=30&mode=crop", + avatarUrl + "?width=60&height=60&mode=crop", + avatarUrl + "?width=90&height=90&mode=crop", + avatarUrl + "?width=150&height=150&mode=crop", + avatarUrl + "?width=300&height=300&mode=crop" + }; + + } + /// /// Returns the culture info associated with this user, based on the language they're assigned to in the back office /// diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 4f3fb1b6e7..296985f084 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -269,7 +269,8 @@ namespace Umbraco.Core.Persistence.Repositories {"lastLoginDate", "LastLoginDate"}, {"failedLoginAttempts", "FailedPasswordAttempts"}, {"createDate", "CreateDate"}, - {"updateDate", "UpdateDate"} + {"updateDate", "UpdateDate"}, + {"avatar", "Avatar"} }; //create list of properties that have changed diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index c70729246f..c08fbb0cdf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -11,6 +11,17 @@ function usersResource($http, umbRequestHelper, $q, umbDataFormatter) { + function clearAvatar(userId) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostClearAvatar", + { id: userId })), + 'Failed to clear the user avatar ' + userId); + } + function disableUsers(userIds) { if (!userIds) { throw "userIds not specified"; @@ -188,7 +199,8 @@ createUser: createUser, saveUser: saveUser, getUserGroup: getUserGroup, - getUserGroups: getUserGroups + getUserGroups: getUserGroups, + clearAvatar: clearAvatar }; return resource; diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index 1504abf7c1..f17fefcac8 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -60,7 +60,7 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ $scope.authenticated = data.authenticated; $scope.user = data.user; - updateChecker.check().then(function(update) { + updateChecker.check().then(function (update) { if (update && update !== "null") { if (update.type !== "None") { var notification = { @@ -87,7 +87,7 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ } //if this is a new login (i.e. the user entered credentials), then clear out local storage - could contain sensitive data - if (data.loginType === "credentials") { + if (data.loginType === "credentials") { localStorageService.clearAll(); } @@ -96,30 +96,15 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ tmhDynamicLocale.set($scope.user.locale); } - if ($scope.user.emailHash) { + if ($scope.user.avatars) { - //let's attempt to load the avatar, it might not exist or we might not have - // internet access, well get an empty string back - $http.get(umbRequestHelper.getApiUrl("gravatarApiBaseUrl", "GetCurrentUserGravatarUrl")) - .then( - function successCallback(response) { - // if we can't download the gravatar for some reason, an null gets returned, we cannot do anything - if (response.data !== "null") { - if ($scope.user && $scope.user.emailHash) { - var avatarBaseUrl = "https://www.gravatar.com/avatar/"; - var hash = $scope.user.emailHash; + $scope.avatar = []; + if (angular.isArray($scope.user.avatars)) { + for (var i = 0; i < $scope.user.avatars.length; i++) { + $scope.avatar.push({ value: $scope.user.avatars[i] }); + } + } - $scope.avatar = [ - { value: avatarBaseUrl + hash + ".jpg?s=30&d=mm" }, - { value: avatarBaseUrl + hash + ".jpg?s=60&d=mm" }, - { value: avatarBaseUrl + hash + ".jpg?s=90&d=mm" } - ]; - } - } - - }, function errorCallback(response) { - //cannot load it from the server so we cannot do anything - }); } })); @@ -143,7 +128,7 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ //register it angular.module('umbraco').controller("Umbraco.MainController", MainController). - config(function (tmhDynamicLocaleProvider) { - //Set url for locale files - tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js'); - }); + config(function (tmhDynamicLocaleProvider) { + //Set url for locale files + tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js'); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js index 9ce2506e76..b540df524c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js @@ -96,7 +96,7 @@ } } else if (evt.Message) { - file.serverErrorMessage = evt.Message; + vm.zipFile.serverErrorMessage = evt.Message; } } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index c69fe3ad6e..888b14b93f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, $timeout, $location, $routeParams, usersResource, contentEditingHelper, localizationService, notificationsService) { + function UserEditController($scope, $timeout, $location, $routeParams, usersResource, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper) { var vm = this; var localizeSaving = localizationService.localize("general_saving"); @@ -9,6 +9,7 @@ vm.page = {}; vm.user = {}; vm.breadcrumbs = []; + vm.avatarFile = {}; vm.goToPage = goToPage; vm.openUserGroupPicker = openUserGroupPicker; @@ -18,8 +19,10 @@ vm.disableUser = disableUser; vm.resetPassword = resetPassword; vm.getUserStateType = getUserStateType; - vm.changeAvatar = changeAvatar; + vm.clearAvatar = clearAvatar; vm.save = save; + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB" + vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); function init() { @@ -31,7 +34,7 @@ makeBreadcrumbs(vm.user); vm.loading = false; }); - + } function save() { @@ -47,7 +50,7 @@ // when server side validation fails - as opposed to content where we are capable of saving the content // item if server side validation fails redirectOnFailure: false, - rebindCallback: function (orignal, saved) {} + rebindCallback: function (orignal, saved) { } }).then(function (saved) { vm.user = saved; @@ -72,17 +75,17 @@ selection: vm.user.userGroups, closeButtonLabel: "Cancel", show: true, - submit: function(model) { + submit: function (model) { // apply changes - if(model.selection) { + if (model.selection) { vm.user.userGroups = model.selection; } vm.userGroupPicker.show = false; vm.userGroupPicker = null; }, - close: function(oldModel) { + close: function (oldModel) { // rollback on close - if(oldModel.selection) { + if (oldModel.selection) { vm.user.userGroups = oldModel.selection; } vm.userGroupPicker.show = false; @@ -97,14 +100,14 @@ view: "contentpicker", multiPicker: true, show: true, - submit: function(model) { - if(model.selection) { + submit: function (model) { + if (model.selection) { vm.user.startNodesContent = model.selection; } vm.contentPicker.show = false; vm.contentPicker = null; }, - close: function(oldModel) { + close: function (oldModel) { vm.contentPicker.show = false; vm.contentPicker = null; } @@ -120,14 +123,14 @@ entityType: "media", multiPicker: true, show: true, - submit: function(model) { - if(model.selection) { + submit: function (model) { + if (model.selection) { vm.user.startNodesMedia = model.selection; } vm.contentPicker.show = false; vm.contentPicker = null; }, - close: function(oldModel) { + close: function (oldModel) { vm.contentPicker.show = false; vm.contentPicker = null; } @@ -144,6 +147,13 @@ function resetPassword() { alert("reset password"); + } + + function clearAvatar() { + // get user + usersResource.clearAvatar(vm.user.id).then(function (data) { + vm.user.avatars = data; + }); } function getUserStateType(state) { @@ -157,10 +167,63 @@ } } - function changeAvatar() { - alert("change avatar"); + $scope.changeAvatar = function (files, event) { + if (files && files.length > 0) { + upload(files[0]); + } + }; + + function upload(file) { + + Upload.upload({ + url: umbRequestHelper.getApiUrl("userApiBaseUrl", "PostSetAvatar", { id: vm.user.id }), + fields: {}, + file: file + }).progress(function (evt) { + + //TODO: Do progress, etc... + // set uploading status on file + vm.avatarFile.uploadStatus = "uploading"; + + }).success(function (data, status, headers, config) { + + // set done status on file + vm.avatarFile.uploadStatus = "done"; + + vm.user.avatars = data; + + }).error(function (evt, status, headers, config) { + + // set status done + vm.avatarFile.uploadStatus = "error"; + + // If file not found, server will return a 404 and display this message + if (status === 404) { + vm.avatarFile.serverErrorMessage = "File not found"; + } + else if (status == 400) { + //it's a validation error + vm.avatarFile.serverErrorMessage = evt.message; + } + else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + vm.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { + vm.avatarFile.serverErrorMessage = "File too large to upload"; + } + + } else if (evt.Message) { + vm.avatarFile.serverErrorMessage = evt.Message; + } + } + }); } + function makeBreadcrumbs() { vm.breadcrumbs = [ { @@ -173,7 +236,7 @@ } ]; } - + init(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index a703a45139..0c6ded037a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -119,18 +119,30 @@
- + + - - - + img-src="{{vm.user.avatars[4]}}"> + + + Change avatar +
+ + Clear avatar + +
Last login:
@@ -181,10 +178,11 @@
@@ -192,9 +190,10 @@
From 271c771f1c78c5ed25f15c9d3257a09df3d2e2ea Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 May 2017 09:50:51 +1000 Subject: [PATCH 106/714] fixing tests --- src/Umbraco.Core/Models/Membership/User.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index e3c4d910af..5bb954ec81 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -73,12 +73,14 @@ namespace Umbraco.Core.Models.Membership /// public User(int id, string name, string email, string username, string rawPasswordValue, IEnumerable allowedSections, IEnumerable userGroups) : this() - { + { + //we allow whitespace for this value so just check null + if (rawPasswordValue == null) throw new ArgumentNullException("rawPasswordValue"); if (allowedSections == null) throw new ArgumentNullException("allowedSections"); if (userGroups == null) throw new ArgumentNullException("userGroups"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username"); - if (string.IsNullOrWhiteSpace(rawPasswordValue)) throw new ArgumentException("Value cannot be null or whitespace.", "rawPasswordValue"); + Id = id; _name = name; From 4337036358ad6bd51d8aff7b8388f30748f52d08 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 May 2017 11:17:06 +1000 Subject: [PATCH 107/714] refactor users and how groups are added to them so we can know dynamically start nodes, etc... fixes tests --- .../Models/Membership/IReadOnlyUserGroup.cs | 20 ++++++ src/Umbraco.Core/Models/Membership/IUser.cs | 4 +- .../Models/Membership/IUserGroup.cs | 9 +-- .../Models/Membership/ReadOnlyUserGroup.cs | 51 +++++++++++++++ src/Umbraco.Core/Models/Membership/User.cs | 54 ++++++++-------- .../Models/Membership/UserGroupExtensions.cs | 18 ++++++ src/Umbraco.Core/Models/UserExtensions.cs | 14 +++-- .../Persistence/Factories/UserFactory.cs | 18 +----- .../Repositories/UserRepository.cs | 4 +- .../Security/BackOfficeUserStore.cs | 19 +++--- src/Umbraco.Core/Services/UserService.cs | 4 +- src/Umbraco.Core/Umbraco.Core.csproj | 3 + .../Repositories/UserRepositoryTest.cs | 62 ++++++++++++++++--- .../UmbracoBackOfficeIdentityTests.cs | 6 +- .../Services/ContentServiceTests.cs | 2 +- .../Services/UserServiceTests.cs | 6 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 8 +-- .../ContentControllerUnitTests.cs | 2 +- ...terAllowedOutgoingContentAttributeTests.cs | 2 +- .../MediaControllerUnitTests.cs | 2 +- .../Controllers/UsersControllerTests.cs | 20 +----- .../Models/Mapping/UserModelMapper.cs | 26 +++----- src/umbraco.businesslogic/User.cs | 6 +- 23 files changed, 232 insertions(+), 128 deletions(-) create mode 100644 src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs create mode 100644 src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs create mode 100644 src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs rename src/Umbraco.Tests/Web/Controllers/{WebApiEditors => }/ContentControllerUnitTests.cs (99%) rename src/Umbraco.Tests/Web/Controllers/{WebApiEditors => }/FilterAllowedOutgoingContentAttributeTests.cs (98%) rename src/Umbraco.Tests/Web/Controllers/{WebApiEditors => }/MediaControllerUnitTests.cs (98%) rename src/Umbraco.Tests/{ => Web}/Controllers/UsersControllerTests.cs (93%) diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs new file mode 100644 index 0000000000..2498e1f70e --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Membership +{ + /// + /// A readonly user group providing basic information + /// + public interface IReadOnlyUserGroup + { + int StartContentId { get; } + int StartMediaId { get; } + + /// + /// The alias + /// + string Alias { get; } + + IEnumerable AllowedSections { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index d6133c04dc..d6af54f937 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -20,11 +20,11 @@ namespace Umbraco.Core.Models.Membership /// /// Gets the groups that user is part of /// - IEnumerable Groups { get; } + IEnumerable Groups { get; } void RemoveGroup(string group); void ClearGroups(); - void AddGroup(string group); + void AddGroup(IReadOnlyUserGroup group); IEnumerable AllowedSections { get; } diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index d2c58883a2..106f52f569 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -5,6 +5,8 @@ namespace Umbraco.Core.Models.Membership { public interface IUserGroup : IAggregateRoot { + string Alias { get; set; } + int StartContentId { get; set; } int StartMediaId { get; set; } @@ -12,12 +14,7 @@ namespace Umbraco.Core.Models.Membership /// The icon ///
string Icon { get; set; } - - /// - /// The alias - /// - string Alias { get; set; } - + /// /// The name /// diff --git a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs new file mode 100644 index 0000000000..68c2f094b9 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Membership +{ + public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable + { + public ReadOnlyUserGroup(int startContentId, int startMediaId, string @alias, IEnumerable allowedSections) + { + StartContentId = startContentId; + StartMediaId = startMediaId; + Alias = alias; + AllowedSections = allowedSections; + } + + public int StartContentId { get; private set; } + public int StartMediaId { get; private set; } + public string Alias { get; private set; } + public IEnumerable AllowedSections { get; private set; } + + public bool Equals(ReadOnlyUserGroup other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Alias, other.Alias); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ReadOnlyUserGroup) obj); + } + + public override int GetHashCode() + { + return Alias.GetHashCode(); + } + + public static bool operator ==(ReadOnlyUserGroup left, ReadOnlyUserGroup right) + { + return Equals(left, right); + } + + public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) + { + return !Equals(left, right); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 5bb954ec81..db55622118 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Models.Membership public User() { SessionTimeout = 60; - _userGroups = new List(); + _userGroups = new HashSet(); _language = GlobalSettings.DefaultUILanguage; _isApproved = true; _isLockedOut = false; @@ -31,7 +31,6 @@ namespace Umbraco.Core.Models.Membership _startMediaIds = new int[] { }; //cannot be null _rawPasswordValue = ""; - _allowedSections = new List(); } /// @@ -53,8 +52,7 @@ namespace Umbraco.Core.Models.Membership _email = email; _username = username; _rawPasswordValue = rawPasswordValue; - _allowedSections = new List(); - _userGroups = new List(); + _userGroups = new HashSet(); _isApproved = true; _isLockedOut = false; _startContentIds = new int[] { }; @@ -69,26 +67,22 @@ namespace Umbraco.Core.Models.Membership /// /// /// - /// /// - public User(int id, string name, string email, string username, string rawPasswordValue, IEnumerable allowedSections, IEnumerable userGroups) + public User(int id, string name, string email, string username, string rawPasswordValue, IEnumerable userGroups) : this() { //we allow whitespace for this value so just check null if (rawPasswordValue == null) throw new ArgumentNullException("rawPasswordValue"); - if (allowedSections == null) throw new ArgumentNullException("allowedSections"); if (userGroups == null) throw new ArgumentNullException("userGroups"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username"); - + if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username"); Id = id; _name = name; _email = email; _username = username; _rawPasswordValue = rawPasswordValue; - _allowedSections = allowedSections; - _userGroups = new List(userGroups); + _userGroups = new HashSet(userGroups); _isApproved = true; _isLockedOut = false; _startContentIds = new int[] { }; @@ -107,7 +101,7 @@ namespace Umbraco.Core.Models.Membership private string _email; private string _rawPasswordValue; private IEnumerable _allowedSections; - private List _userGroups; + private HashSet _userGroups; private bool _isApproved; private bool _isLockedOut; private string _language; @@ -142,7 +136,7 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo DefaultToLiveEditingSelector = ExpressionHelper.GetPropertyInfo(x => x.DefaultToLiveEditing); - public readonly PropertyInfo UserGroupsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Groups); + public readonly PropertyInfo UserGroupsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Groups); //Custom comparer for enumerable public readonly DelegateEqualityComparer> IntegerEnumerableComparer = @@ -258,7 +252,7 @@ namespace Umbraco.Core.Models.Membership public IEnumerable AllowedSections { - get { return _allowedSections; } + get { return _allowedSections ?? (_allowedSections = new List(_userGroups.SelectMany(x => x.AllowedSections).Distinct())); } } public IProfile ProfileData @@ -339,20 +333,25 @@ namespace Umbraco.Core.Models.Membership } /// - /// Gets or sets the groups that user is part of + /// Gets the groups that user is part of /// [DataMember] - public IEnumerable Groups + public IEnumerable Groups { get { return _userGroups; } } public void RemoveGroup(string group) { - if (_userGroups.Contains(group)) + foreach (var userGroup in _userGroups.ToArray()) { - _userGroups.Remove(group); - OnPropertyChanged(Ps.Value.UserGroupsSelector); + if (userGroup.Alias == group) + { + _userGroups.Remove(userGroup); + //reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; + OnPropertyChanged(Ps.Value.UserGroupsSelector); + } } } @@ -361,17 +360,20 @@ namespace Umbraco.Core.Models.Membership if (_userGroups.Count > 0) { _userGroups.Clear(); + //reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; OnPropertyChanged(Ps.Value.UserGroupsSelector); } } - public void AddGroup(string group) + public void AddGroup(IReadOnlyUserGroup group) { - if (_userGroups.Contains(group) == false) - { - _userGroups.Add(group); + if (_userGroups.Add(group)) + { + //reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; OnPropertyChanged(Ps.Value.UserGroupsSelector); - } + } } #endregion @@ -385,8 +387,8 @@ namespace Umbraco.Core.Models.Membership //turn off change tracking clone.DisableChangeTracking(); //need to create new collections otherwise they'll get copied by ref - clone._userGroups = new List(_userGroups); - clone._allowedSections = new List(_allowedSections); + clone._userGroups = new HashSet(_userGroups); + clone._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; //re-create the event handler //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs new file mode 100644 index 0000000000..ec39fb0bf4 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs @@ -0,0 +1,18 @@ +using Umbraco.Core.Models.Rdbms; +using System.Linq; + +namespace Umbraco.Core.Models.Membership +{ + internal static class UserGroupExtensions + { + public static IReadOnlyUserGroup ToReadOnlyGroup(this IUserGroup group) + { + return new ReadOnlyUserGroup(group.StartContentId, group.StartMediaId, group.Alias, group.AllowedSections); + } + + public static IReadOnlyUserGroup ToReadOnlyGroup(this UserGroupDto group) + { + return new ReadOnlyUserGroup(group.StartContentId, group.StartMediaId, group.Alias, group.UserGroup2AppDtos.Select(x => x.AppAlias).ToArray()); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 6636259c92..198c41bd54 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -125,12 +125,16 @@ namespace Umbraco.Core.Models var formattedPath = "," + path + ","; var formattedRecycleBinId = "," + recycleBinId.ToInvariantString() + ","; - //only users with root access have access to the recycle bin + //check for root path access + //TODO: This logic may change + if (startNodeIds.Length == 0 || startNodeIds.Contains(Constants.System.Root)) + return true; + + //only users with root access have access to the recycle bin so if the above check didn't pass than access is denied if (formattedPath.Contains(formattedRecycleBinId)) { - var hasAccess = startNodeIds.Length == 0 || startNodeIds.Contains(Constants.System.Root); - return hasAccess; - } + return false; + } //check for normal paths foreach (var startNodeId in startNodeIds) @@ -168,7 +172,7 @@ namespace Umbraco.Core.Models public static bool IsAdmin(this IUser user) { if (user == null) throw new ArgumentNullException("user"); - return user.Groups != null && user.Groups.Any(x => x == Constants.Security.AdminGroupAlias); + return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.AdminGroupAlias); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 64a63447d9..2915bc570e 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -11,21 +11,9 @@ namespace Umbraco.Core.Persistence.Factories { public static IUser BuildEntity(UserDto dto) { - var guidId = dto.Id.ToGuid(); - - var groupAliases = new HashSet(); - var allowedSections = new HashSet(); - var userGroups = dto.UserGroupDtos; - foreach (var userGroup in userGroups) - { - groupAliases.Add(userGroup.Alias); - foreach (var section in userGroup.UserGroup2AppDtos) - { - allowedSections.Add(section.AppAlias); - } - } - - var user = new User(dto.Id, dto.UserName, dto.Email, dto.Login,dto.Password, allowedSections, groupAliases); + var guidId = dto.Id.ToGuid(); + + var user = new User(dto.Id, dto.UserName, dto.Email, dto.Login,dto.Password, dto.UserGroupDtos.Select(x => x.ToReadOnlyGroup()).ToArray()); try { diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 296985f084..96c3575596 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -221,7 +221,7 @@ namespace Umbraco.Core.Persistence.Repositories //lookup all assigned var assigned = entity.Groups == null || entity.Groups.Any() == false ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups }); + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); foreach (var groupDto in assigned) { @@ -318,7 +318,7 @@ namespace Umbraco.Core.Persistence.Repositories //lookup all assigned var assigned = entity.Groups == null || entity.Groups.Any() == false ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups }); + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); //first delete all //TODO: We could do this a nicer way instead of "Nuke and Pave" diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 611242b088..d8bf69641d 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -408,7 +408,7 @@ namespace Umbraco.Core.Security if (foundUser != null && foundGroup != null) { - foundUser.AddGroup(foundGroup.Alias); + foundUser.AddGroup(foundGroup.ToReadOnlyGroup()); } return Task.FromResult(0); @@ -688,22 +688,21 @@ namespace Umbraco.Core.Security anythingChanged = true; user.SecurityStamp = identityUser.SecurityStamp; } - - if (user.Groups.ContainsAll(identityUser.Groups) == false - || identityUser.Groups.ContainsAll(user.Groups) == false) + + var userGroups = user.Groups.Select(x => x.Alias).ToArray(); + if (userGroups.ContainsAll(identityUser.Groups) == false + || identityUser.Groups.ContainsAll(userGroups) == false) { anythingChanged = true; - + //clear out the current groups (need to ToArray since we are modifying the iterator) - foreach (var group in user.Groups.ToArray()) - { - user.RemoveGroup(group); - } + user.ClearGroups(); + //get all of the ones found by alias and add them var foundGroups = _userService.GetUserGroupsByAlias(identityUser.Groups); foreach (var group in foundGroups) { - user.AddGroup(group.Alias); + user.AddGroup(group.ToReadOnlyGroup()); } } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index a801986b60..dd81824f6a 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -825,7 +825,7 @@ namespace Umbraco.Core.Services foreach (var group in user.Groups) { //TODO: This may perform horribly :/ - foreach (var permission in GetPermissions(group, false, nodeIds)) + foreach (var permission in GetPermissions(group.Alias, false, nodeIds)) { AddOrAmendPermissionList(result, permission); } @@ -922,7 +922,7 @@ namespace Umbraco.Core.Services /// String indicating permissions for provided user and path public string GetPermissionsForPath(IUser user, string path) { - var assignedPermissions = GetPermissionsForGroupsAndPath(user.Groups, path); + var assignedPermissions = GetPermissionsForGroupsAndPath(user.Groups.Select(x => x.Alias), path); return GetAggregatePermissions(assignedPermissions); } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 52a367224b..3b6caadbcb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -343,6 +343,9 @@ + + + diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index a4cf866cd2..88ecb50f2f 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -2,6 +2,8 @@ using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; @@ -28,6 +30,29 @@ namespace Umbraco.Tests.Persistence.Repositories base.TearDown(); } + private MediaRepository CreateMediaRepository(IScopeUnitOfWork unitOfWork, out IMediaTypeRepository mediaTypeRepository) + { + mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); + var tagRepository = new TagRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); + var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); + return repository; + } + + private ContentRepository CreateContentRepository(IScopeUnitOfWork unitOfWork, out IContentTypeRepository contentTypeRepository) + { + ITemplateRepository tr; + return CreateContentRepository(unitOfWork, out contentTypeRepository, out tr); + } + + private ContentRepository CreateContentRepository(IScopeUnitOfWork unitOfWork, out IContentTypeRepository contentTypeRepository, out ITemplateRepository templateRepository) + { + templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); + var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); + contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); + var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + return repository; + } + private UserRepository CreateRepository(IScopeUnitOfWork unitOfWork) { var repository = new UserRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); @@ -75,7 +100,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repository = CreateRepository(unitOfWork)) { IUser user = MockedUser.CreateUser(); - user.AddGroup(group.Alias); + user.AddGroup(group.ToReadOnlyGroup()); // Act repository.AddOrUpdate(user); @@ -86,7 +111,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert Assert.That(user.HasIdentity, Is.True); Assert.AreEqual(1, user.Groups.Count()); - Assert.AreEqual(group.Alias, user.Groups.ElementAt(0)); + Assert.AreEqual(group.Alias, user.Groups.ElementAt(0).Alias); } } @@ -137,30 +162,49 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Can_Perform_Update_On_UserRepository() { + var ct = MockedContentTypes.CreateBasicContentType("test"); + var content = MockedContent.CreateBasicContent(ct); + var mt = MockedContentTypes.CreateSimpleMediaType("testmedia", "TestMedia"); + var media = MockedMedia.CreateSimpleMedia(mt, "asdf", -1); + // Arrange var provider = new PetaPocoUnitOfWorkProvider(Logger); var unitOfWork = provider.GetUnitOfWork(); - using (var repository = CreateRepository(unitOfWork)) + IContentTypeRepository contentTypeRepo; + IMediaTypeRepository mediaTypeRepo; + using (var contentRepository = CreateContentRepository(unitOfWork, out contentTypeRepo)) + using (var mediaRepository = CreateMediaRepository(unitOfWork, out mediaTypeRepo)) + using (contentTypeRepo) + using(mediaTypeRepo) + using (var userRepository = CreateRepository(unitOfWork)) using (var userGroupRepository = CreateUserGroupRepository(unitOfWork)) { - var user = CreateAndCommitUserWithGroup(repository, userGroupRepository, unitOfWork); + contentTypeRepo.AddOrUpdate(ct); + mediaTypeRepo.AddOrUpdate(mt); + unitOfWork.Commit(); + + contentRepository.AddOrUpdate(content); + mediaRepository.AddOrUpdate(media); + unitOfWork.Commit(); + + var user = CreateAndCommitUserWithGroup(userRepository, userGroupRepository, unitOfWork); // Act - var resolved = (User)repository.Get((int)user.Id); + var resolved = (User)userRepository.Get((int)user.Id); resolved.Name = "New Name"; resolved.Language = "fr"; resolved.IsApproved = false; resolved.RawPasswordValue = "new"; resolved.IsLockedOut = true; - resolved.StartContentIds = new []{ 10 }; - resolved.StartMediaIds = new []{ 11 }; + resolved.StartContentIds = new[] { content.Id }; + resolved.StartMediaIds = new[] { media.Id }; resolved.Email = "new@new.com"; resolved.Username = "newName"; - repository.AddOrUpdate(resolved); + userRepository.AddOrUpdate(resolved); unitOfWork.Commit(); - var updatedItem = (User)repository.Get((int)user.Id); + var updatedItem = (User)userRepository.Get((int)user.Id); // Assert Assert.That(updatedItem.Id, Is.EqualTo(resolved.Id)); diff --git a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs index 226b26990d..634c2e6f5a 100644 --- a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs @@ -29,8 +29,8 @@ namespace Umbraco.Tests.Security //This is the id that 'identity' uses to check for the username new Claim(ClaimTypes.Name, "testing", ClaimValueTypes.String, TestIssuer, TestIssuer), new Claim(ClaimTypes.GivenName, "hello world", ClaimValueTypes.String, TestIssuer, TestIssuer), - new Claim(Constants.Security.StartContentNodeIdClaimType, "-1", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), - new Claim(Constants.Security.StartMediaNodeIdClaimType, "5543", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim(Constants.Security.StartContentNodeIdClaimType, "[-1]", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim(Constants.Security.StartMediaNodeIdClaimType, "[5543]", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), new Claim(Constants.Security.AllowedApplicationsClaimType, "content", ClaimValueTypes.String, TestIssuer, TestIssuer), new Claim(Constants.Security.AllowedApplicationsClaimType, "media", ClaimValueTypes.String, TestIssuer, TestIssuer), new Claim(ClaimTypes.Locality, "en-us", ClaimValueTypes.String, TestIssuer, TestIssuer), @@ -44,7 +44,7 @@ namespace Umbraco.Tests.Security Assert.AreEqual(sessionId, backofficeIdentity.SessionId); Assert.AreEqual("testing", backofficeIdentity.Username); Assert.AreEqual("hello world", backofficeIdentity.RealName); - Assert.AreEqual(0, backofficeIdentity.StartContentNodes.Length); + Assert.AreEqual(1, backofficeIdentity.StartContentNodes.Length); Assert.IsTrue(backofficeIdentity.StartMediaNodes.UnsortedSequenceEqual(new []{ 5543 })); Assert.IsTrue(new[] {"content", "media"}.SequenceEqual(backofficeIdentity.AllowedApplications)); Assert.AreEqual("en-us", backofficeIdentity.Culture); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 652152273d..1690bfdbee 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1404,7 +1404,7 @@ namespace Umbraco.Tests.Services Assert.IsTrue(ServiceContext.PublicAccessService.AddRule(content1, "test2", "test2").Success); var user = ServiceContext.UserService.GetUserById(0); - var userGroup = ServiceContext.UserService.GetUserGroupByAlias(user.Groups.First()); + var userGroup = ServiceContext.UserService.GetUserGroupByAlias(user.Groups.First().Alias); Assert.IsNotNull(ServiceContext.NotificationService.CreateNotification(user, content1, "test")); ServiceContext.ContentService.AssignContentPermission(content1, 'A', new[] { userGroup.Id}); diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 5a0ad30169..0ac00d0fb4 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -337,7 +337,7 @@ namespace Umbraco.Tests.Services var users = MockedUser.CreateMulipleUsers(10).ToArray(); for (var i = 0; i < 10;) { - users[i].AddGroup(userGroup.Alias); + users[i].AddGroup(userGroup.ToReadOnlyGroup()); i = i + 2; } ServiceContext.UserService.Save(users); @@ -360,7 +360,7 @@ namespace Umbraco.Tests.Services var users = MockedUser.CreateMulipleUsers(10).ToArray(); for (var i = 0; i < 10;) { - users[i].AddGroup(userGroup.Alias); + users[i].AddGroup(userGroup.ToReadOnlyGroup()); i = i + 2; } for (var i = 0; i < 10;) @@ -697,7 +697,7 @@ namespace Umbraco.Tests.Services userGroup = CreateTestUserGroup(); var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com"); - user.AddGroup(userGroup.Alias); + user.AddGroup(userGroup.ToReadOnlyGroup()); ServiceContext.UserService.Save(user); return user; } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 651ae301ce..7d5dcc0fd5 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -190,7 +190,7 @@ - + @@ -368,9 +368,9 @@ - - - + + + diff --git a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs similarity index 99% rename from src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs rename to src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs index 7782612420..ae735be64e 100644 --- a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Editors; -namespace Umbraco.Tests.Web.Controllers.WebApiEditors +namespace Umbraco.Tests.Web.Controllers { [TestFixture] public class ContentControllerUnitTests diff --git a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs b/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs similarity index 98% rename from src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs rename to src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs index bc4046b39e..b93d503d3d 100644 --- a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.WebApi.Filters; -namespace Umbraco.Tests.Web.Controllers.WebApiEditors +namespace Umbraco.Tests.Web.Controllers { [TestFixture] public class FilterAllowedOutgoingContentAttributeTests diff --git a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/MediaControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/MediaControllerUnitTests.cs similarity index 98% rename from src/Umbraco.Tests/Web/Controllers/WebApiEditors/MediaControllerUnitTests.cs rename to src/Umbraco.Tests/Web/Controllers/MediaControllerUnitTests.cs index e65b1a1b19..7923920fe7 100644 --- a/src/Umbraco.Tests/Web/Controllers/WebApiEditors/MediaControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/MediaControllerUnitTests.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Editors; -namespace Umbraco.Tests.Web.Controllers.WebApiEditors +namespace Umbraco.Tests.Web.Controllers { [TestFixture] public class MediaControllerUnitTests diff --git a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs similarity index 93% rename from src/Umbraco.Tests/Controllers/UsersControllerTests.cs rename to src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index 7af3d5e340..ad09e241c2 100644 --- a/src/Umbraco.Tests/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -1,23 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.IO; -using System.Linq; -using System.Net; +using System.Linq; using System.Net.Http; using System.Net.Http.Formatting; -using System.Net.Http.Headers; -using System.Text; -using System.Web.Configuration; -using System.Web.Http; -using System.Web.Http.SelfHost; -using AutoMapper; -using Examine.Providers; -using Microsoft.Owin.Testing; using Moq; using Newtonsoft.Json; using NUnit.Framework; -using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -26,10 +12,8 @@ using Umbraco.Tests.TestHelpers.ControllerTesting; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; -using Umbraco.Web.WebApi; -namespace Umbraco.Tests.Controllers +namespace Umbraco.Tests.Web.Controllers { [DatabaseTestBehavior(DatabaseBehavior.NoDatabasePerFixture)] [TestFixture] diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 5669bbaac8..81ac54caae 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -40,9 +40,10 @@ namespace Umbraco.Web.Models.Mapping .AfterMap((save, user) => { user.ClearGroups(); - foreach (var group in save.UserGroups) + var foundGroups = applicationContext.Services.UserService.GetUserGroupsByAlias(save.UserGroups.ToArray()); + foreach (var group in foundGroups) { - user.AddGroup(group); + user.AddGroup(group.ToReadOnlyGroup()); } }); @@ -72,10 +73,12 @@ namespace Umbraco.Web.Models.Mapping .ForMember(user => user.UpdateDate, expression => expression.Ignore()) .AfterMap((invite, user) => { - foreach (var group in invite.UserGroups) + user.ClearGroups(); + var foundGroups = applicationContext.Services.UserService.GetUserGroupsByAlias(invite.UserGroups.ToArray()); + foreach (var group in foundGroups) { - user.AddGroup(group); - } + user.AddGroup(group.ToReadOnlyGroup()); + } }); config.CreateMap() @@ -123,18 +126,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember( detail => detail.EmailHash, opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) - .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()); - - config.CreateMap() - .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => user.Id)) - .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds)) - .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds)) - .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture)) - .ForMember(detail => detail.AllowedSections, opt => opt.MapFrom(user => user.AllowedSections)) - .ForMember( - detail => detail.EmailHash, - opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) - .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()); + .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()); config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 54748f7637..fbf226c39f 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -657,7 +657,9 @@ namespace umbraco.BusinessLogic public void AddGroup(string groupAlias) { if (_lazyId.HasValue) SetupUser(_lazyId.Value); - UserEntity.AddGroup(groupAlias); + var group = ApplicationContext.Current.Services.UserService.GetUserGroupByAlias(groupAlias); + if (group != null) + UserEntity.AddGroup(group.ToReadOnlyGroup()); } /// @@ -667,7 +669,7 @@ namespace umbraco.BusinessLogic public string[] GetGroups() { if (_lazyId.HasValue) SetupUser(_lazyId.Value); - return UserEntity.Groups.ToArray(); + return UserEntity.Groups.Select(x => x.Alias).ToArray(); } From 362b98509b719af9cd78f6a8fa6e3de6c5102090 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 May 2017 12:09:06 +1000 Subject: [PATCH 108/714] Updates path check to use all assigned user start nodes that are combined by their groups and directly assigned values --- src/Umbraco.Core/Models/Membership/User.cs | 12 ++++++--- src/Umbraco.Core/Models/UserExtensions.cs | 25 ++++++++++++++++--- .../Persistence/Factories/UserFactory.cs | 7 +++--- .../Models/Mapping/UserModelMapper.cs | 2 +- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index db55622118..b2e380811b 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -57,7 +57,7 @@ namespace Umbraco.Core.Models.Membership _isLockedOut = false; _startContentIds = new int[] { }; _startMediaIds = new int[] { }; - } + } /// /// Constructor for creating a new User instance for an existing user @@ -68,12 +68,16 @@ namespace Umbraco.Core.Models.Membership /// /// /// - public User(int id, string name, string email, string username, string rawPasswordValue, IEnumerable userGroups) + /// + /// + public User(int id, string name, string email, string username, string rawPasswordValue, IEnumerable userGroups, int[] startContentIds, int[] startMediaIds) : this() { //we allow whitespace for this value so just check null if (rawPasswordValue == null) throw new ArgumentNullException("rawPasswordValue"); if (userGroups == null) throw new ArgumentNullException("userGroups"); + if (startContentIds == null) throw new ArgumentNullException("startContentIds"); + if (startMediaIds == null) throw new ArgumentNullException("startMediaIds"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username"); @@ -85,8 +89,8 @@ namespace Umbraco.Core.Models.Membership _userGroups = new HashSet(userGroups); _isApproved = true; _isLockedOut = false; - _startContentIds = new int[] { }; - _startMediaIds = new int[] { }; + _startContentIds = startContentIds; + _startMediaIds = startMediaIds; } private string _name; diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 198c41bd54..4209878116 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -10,7 +11,25 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Models { public static class UserExtensions - { + { + /// + /// Returns all of the user's assigned start node ids based on ids assigned directly to the IUser object and it's groups + /// + /// + public static IEnumerable GetCombinedStartContentIds(this IUser user) + { + return user.StartContentIds.Concat(user.Groups.Select(x => x.StartContentId)).Distinct(); + } + + /// + /// Returns all of the user's assigned start node ids based on ids assigned directly to the IUser object and it's groups + /// + /// + public static IEnumerable GetCombinedMediaContentIds(this IUser user) + { + return user.StartMediaIds.Concat(user.Groups.Select(x => x.StartMediaId)).Distinct(); + } + /// /// Tries to lookup the user's gravatar to see if the endpoint can be reached, if so it returns the valid URL /// @@ -115,7 +134,7 @@ namespace Umbraco.Core.Models { if (user == null) throw new ArgumentNullException("user"); if (content == null) throw new ArgumentNullException("content"); - return HasPathAccess(content.Path, user.StartContentIds, Constants.System.RecycleBinContent); + return HasPathAccess(content.Path, user.GetCombinedStartContentIds().ToArray(), Constants.System.RecycleBinContent); } internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) @@ -159,7 +178,7 @@ namespace Umbraco.Core.Models { if (user == null) throw new ArgumentNullException("user"); if (media == null) throw new ArgumentNullException("media"); - return HasPathAccess(media.Path, user.StartMediaIds, Constants.System.RecycleBinMedia); + return HasPathAccess(media.Path, user.GetCombinedMediaContentIds().ToArray(), Constants.System.RecycleBinMedia); } /// diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 2915bc570e..cae42191c6 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -13,15 +13,16 @@ namespace Umbraco.Core.Persistence.Factories { var guidId = dto.Id.ToGuid(); - var user = new User(dto.Id, dto.UserName, dto.Email, dto.Login,dto.Password, dto.UserGroupDtos.Select(x => x.ToReadOnlyGroup()).ToArray()); + var user = new User(dto.Id, dto.UserName, dto.Email, dto.Login,dto.Password, + dto.UserGroupDtos.Select(x => x.ToReadOnlyGroup()).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.StartNode).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.StartNode).ToArray()); try { user.DisableChangeTracking(); user.Key = guidId; - user.StartContentIds = dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.StartNode).ToArray(); - user.StartMediaIds = dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int) UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.StartNode).ToArray(); user.IsLockedOut = dto.NoConsole; user.IsApproved = dto.Disabled == false; user.Language = dto.UserLanguage; diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 81ac54caae..339de9387e 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -94,7 +94,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(applicationContext.Services.UserService, applicationContext.ApplicationCache.RuntimeCache))) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) - .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) + .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups.Select(x => x.Alias).ToArray())) .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds)) .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) From f493c254e802bec45e1cc7b543e10216a8e4bcf8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 May 2017 12:47:31 +1000 Subject: [PATCH 109/714] Fixes up the users login date on the user list view, fixes tests --- .../Web/Controllers/UsersControllerTests.cs | 33 ++++++++++++------- .../src/views/users/user.html | 4 +-- .../users/views/users/users.controller.js | 4 +-- .../src/views/users/views/users/users.html | 2 +- .../Models/ContentEditing/UserDisplay.cs | 2 +- .../Models/Mapping/UserModelMapper.cs | 1 + 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index ad09e241c2..32a04350ee 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Net.Http.Formatting; using Moq; @@ -36,7 +37,11 @@ namespace Umbraco.Tests.Web.Controllers u.Id = 1234; }); userServiceMock.Setup(service => service.GetAllUserGroups(It.IsAny())) - .Returns(Enumerable.Empty); + .Returns(new[] { Mock.Of(group => group.Id == 123 && group.Alias == "writers" && group.Name == "Writers") }); + userServiceMock.Setup(service => service.GetUserGroupsByAlias(It.IsAny())) + .Returns(new[] { Mock.Of(group => group.Id == 123 && group.Alias == "writers" && group.Name == "Writers") }); + userServiceMock.Setup(service => service.GetUserById(It.IsAny())) + .Returns(new User(1234, "Test", "asdf@asdf.com", "test", "", new List(), new int[0], new int[0])); //we need to manually apply automapper mappings with the mocked applicationcontext InitializeMappers(helper.UmbracoContext.Application); @@ -44,30 +49,32 @@ namespace Umbraco.Tests.Web.Controllers return new UsersController(helper.UmbracoContext); }); - var invite = new UserInvite + var userSave = new UserSave { Id = -1, Email = "test@test.com", - Message = "Hello test!", + Username = "test@test.com", + Culture = "en", Name = "Test", UserGroups = new[] { "writers" } }; var response = await runner.Execute("Users", "PostSaveUser", HttpMethod.Post, - new ObjectContent(invite, new JsonMediaTypeFormatter())); + new ObjectContent(userSave, new JsonMediaTypeFormatter())); var obj = JsonConvert.DeserializeObject(response.Item2); - Assert.AreEqual(invite.Name, obj.Name); + Assert.AreEqual(userSave.Name, obj.Name); Assert.AreEqual(1234, obj.Id); - Assert.AreEqual(invite.Email, obj.Email); - foreach (var group in invite.UserGroups) + Assert.AreEqual(userSave.Email, obj.Email); + foreach (var group in userSave.UserGroups) { Assert.IsTrue(obj.UserGroups.Contains(group)); } } - [Test] - public async void Invite_User() + [TestCase("PostInviteUser")] + [TestCase("PostCreateUser")] + public async void Invite_And_Create(string action) { var runner = new TestRunner((message, helper) => { @@ -82,7 +89,9 @@ namespace Umbraco.Tests.Web.Controllers u.Id = 1234; }); userServiceMock.Setup(service => service.GetAllUserGroups(It.IsAny())) - .Returns(Enumerable.Empty); + .Returns(new[] { Mock.Of(group => group.Id == 123 && group.Alias == "writers" && group.Name == "Writers") }); + userServiceMock.Setup(service => service.GetUserGroupsByAlias(It.IsAny())) + .Returns(new[]{Mock.Of(group => group.Id == 123 && group.Alias == "writers" && group.Name == "Writers")}); //we need to manually apply automapper mappings with the mocked applicationcontext InitializeMappers(helper.UmbracoContext.Application); @@ -98,7 +107,7 @@ namespace Umbraco.Tests.Web.Controllers Name = "Test", UserGroups = new[] {"writers"} }; - var response = await runner.Execute("Users", "PostInviteUser", HttpMethod.Post, + var response = await runner.Execute("Users", action, HttpMethod.Post, new ObjectContent(invite, new JsonMediaTypeFormatter())); var obj = JsonConvert.DeserializeObject(response.Item2); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index a20a985088..840dde1704 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -160,8 +160,8 @@
Last login:
- {{ vm.user.lastLogin }} - The user has not logged in yet + {{ vm.user.lastLoginDate }} + The user has not logged in yet
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 81cb3737e5..3635521fea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -285,8 +285,8 @@ function formatDates(users) { angular.forEach(users, function(user){ - if(user.lastLogin) { - user.formattedLastLogin = moment(user.lastLogin).format("MMMM Do YYYY, HH:mm"); + if (user.lastLoginDate) { + user.formattedLastLogin = moment(user.lastLoginDate).format("MMMM Do YYYY, HH:mm"); } }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 46432010dc..527f72666c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -140,7 +140,7 @@ diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index b9bea4bba3..f8706f9e97 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.Models.ContentEditing public string EmailHash { get; set; } [DataMember(Name = "lastLoginDate")] - public DateTime LastLoginDate { get; set; } + public DateTime? LastLoginDate { get; set; } /// /// Returns a list of different size avatars diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 339de9387e..bd940a9c57 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -94,6 +94,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(applicationContext.Services.UserService, applicationContext.ApplicationCache.RuntimeCache))) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) + .ForMember(detail => detail.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?)user.LastLoginDate)) .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups.Select(x => x.Alias).ToArray())) .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds)) .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds)) From 7f27745d27af1412aa70f39df9aa1bbdbcd01bfc Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 May 2017 09:36:07 +0200 Subject: [PATCH 110/714] starting to get the invite email with token working --- src/Umbraco.Web/Editors/UsersController.cs | 16 +++++++++++++++- .../Models/Mapping/UserModelMapper.cs | 7 ++++--- .../WebApi/UmbracoAuthorizedApiController.cs | 8 ++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 9de3faefe8..6bfcd5bdbe 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -9,18 +9,22 @@ using System.Web; using System.Web.Http; using AutoMapper; using ClientDependency.Core; +using Microsoft.AspNet.Identity; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; +using IUser = Umbraco.Core.Models.Membership.IUser; namespace Umbraco.Web.Editors { @@ -47,6 +51,8 @@ namespace Umbraco.Web.Editors { } + + /// /// Returns a list of the sizes of gravatar urls for the user or null if the gravatar server cannot be reached /// @@ -229,7 +235,7 @@ namespace Umbraco.Web.Editors /// /// This will email the user an invite and generate a token that will be validated in the email /// - public UserDisplay PostInviteUser(UserInvite userSave) + public async Task PostInviteUser(UserInvite userSave) { if (userSave == null) throw new ArgumentNullException("userSave"); @@ -256,6 +262,14 @@ namespace Umbraco.Web.Editors Services.UserService.Save(user); + //TODO: Send an email! + await UserManager.EmailService.SendAsync(new IdentityMessage + { + Body = "You have been invited to the Umbraco Back Office!", + Destination = userSave., + Subject = "You have been invited to the Umbraco Back Office!" + }); + return Mapper.Map(user); } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index bd940a9c57..cbcb7ab634 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -48,14 +48,15 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() - .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) + .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) + //generate a token for the invite + .ForMember(user => user.SecurityStamp, expression => expression.MapFrom(x => (DateTime.Now + x.Email).ToSHA1())) .ForMember(user => user.Id, expression => expression.Ignore()) .ForMember(user => user.Avatar, expression => expression.Ignore()) .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) .ForMember(user => user.StartContentIds, expression => expression.Ignore()) .ForMember(user => user.StartMediaIds, expression => expression.Ignore()) - .ForMember(user => user.Language, expression => expression.Ignore()) - .ForMember(user => user.SecurityStamp, expression => expression.Ignore()) + .ForMember(user => user.Language, expression => expression.Ignore()) .ForMember(user => user.ProviderUserKey, expression => expression.Ignore()) .ForMember(user => user.Username, expression => expression.Ignore()) .ForMember(user => user.RawPasswordValue, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index 9577e840e1..8328cfa393 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -5,6 +5,8 @@ using Umbraco.Core.Configuration; using Umbraco.Web.Security; using Umbraco.Web.WebApi.Filters; using umbraco.BusinessLogic; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; namespace Umbraco.Web.WebApi { @@ -34,6 +36,12 @@ namespace Umbraco.Web.WebApi { } + private BackOfficeUserManager _userManager; + protected BackOfficeUserManager UserManager + { + get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); } + } + private bool _userisValidated = false; /// From 2087125d958f9b888614b72d630b857b4c683c8b Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 May 2017 14:16:48 +0200 Subject: [PATCH 111/714] Fixes an issue with model mappings and group assignments --- .../Models/Identity/IdentityModelMappings.cs | 2 +- src/Umbraco.Core/Models/Rdbms/UserDto.cs | 4 +-- .../Models/Rdbms/UserStartNodeDto.cs | 35 +++++++++++++++++-- .../Persistence/Relators/UserGroupRelator.cs | 14 ++------ .../Security/BackOfficeUserStore.cs | 3 +- 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 9d01056f48..d48e2e6318 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.StartMediaIds, expression => expression.MapFrom(user => user.StartMediaIds)) .ForMember(user => user.StartContentIds, expression => expression.MapFrom(user => user.StartContentIds)) .ForMember(user => user.AccessFailedCount, expression => expression.MapFrom(user => user.FailedPasswordAttempts)) - .ForMember(user => user.Groups, expression => expression.MapFrom(user => user.Groups.ToArray())) + .ForMember(user => user.Groups, expression => expression.MapFrom(user => user.Groups.Select(x => x.Alias).ToArray())) .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); config.CreateMap() diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 98dee12a94..e804a949da 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Models.Rdbms public UserDto() { UserGroupDtos = new List(); - UserStartNodeDtos = new List(); + UserStartNodeDtos = new HashSet(); } [Column("id")] @@ -91,6 +91,6 @@ namespace Umbraco.Core.Models.Rdbms public List UserGroupDtos { get; set; } [ResultColumn] - public List UserStartNodeDtos { get; set; } + public HashSet UserStartNodeDtos { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs index 42f3e5a7ca..fa8af80759 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Persistence; +using System; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms @@ -6,7 +7,7 @@ namespace Umbraco.Core.Models.Rdbms [TableName("umbracoUserStartNode")] [PrimaryKey("id", autoIncrement = true)] [ExplicitColumns] - internal class UserStartNodeDto + internal class UserStartNodeDto : IEquatable { [Column("id")] [PrimaryKeyColumn(Name = "PK_userStartNode")] @@ -32,5 +33,35 @@ namespace Umbraco.Core.Models.Rdbms Content = 1, Media = 2 } + + public bool Equals(UserStartNodeDto other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id == other.Id; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((UserStartNodeDto) obj); + } + + public override int GetHashCode() + { + return Id; + } + + public static bool operator ==(UserStartNodeDto left, UserStartNodeDto right) + { + return Equals(left, right); + } + + public static bool operator !=(UserStartNodeDto left, UserStartNodeDto right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs b/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs index 435b02c2e3..fdea52498a 100644 --- a/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs +++ b/src/Umbraco.Core/Persistence/Relators/UserGroupRelator.cs @@ -35,7 +35,7 @@ namespace Umbraco.Core.Persistence.Relators // Setup the new current user _currentUser = user; _currentUser.UserGroupDtos = new List(); - _currentUser.UserStartNodeDtos = new List(); + _currentUser.UserStartNodeDtos = new HashSet(); AddOrUpdateGroup(group, section); AddOrUpdateStartNode(startNode); @@ -50,16 +50,8 @@ namespace Umbraco.Core.Persistence.Relators if (startNode == null || startNode.Id == default(int)) return; - //check if this is a new start node - var latestStartNode = _currentUser.UserStartNodeDtos.Count > 0 - ? _currentUser.UserStartNodeDtos[_currentUser.UserStartNodeDtos.Count - 1] - : null; - - if (latestStartNode == null || latestStartNode.Id != startNode.Id) - { - //add the current (new) start node - _currentUser.UserStartNodeDtos.Add(startNode); - } + //add the current (new) start node - this is a hashset so it will only allow unique rows so no need to check for existence + _currentUser.UserStartNodeDtos.Add(startNode); } private void AddOrUpdateGroup(UserGroupDto group, UserGroup2AppDto section) diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index d8bf69641d..3dcb74b62f 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -678,7 +678,7 @@ namespace Umbraco.Core.Security anythingChanged = true; user.StartMediaIds = identityUser.StartMediaIds; } - if (user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds)) + if (user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) { anythingChanged = true; user.StartContentIds = identityUser.StartContentIds; @@ -690,6 +690,7 @@ namespace Umbraco.Core.Security } var userGroups = user.Groups.Select(x => x.Alias).ToArray(); + if (userGroups.ContainsAll(identityUser.Groups) == false || identityUser.Groups.ContainsAll(userGroups) == false) { From 273c934e2639f1820a620557a51db882dd460e6e Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 May 2017 15:13:04 +0200 Subject: [PATCH 112/714] wires up the invite user to the rest endpoint --- src/Umbraco.Core/Security/EmailService.cs | 4 +- .../src/common/resources/users.resource.js | 20 +++- .../src/common/services/util.service.js | 17 +++- .../users/views/users/users.controller.js | 99 ++++++++++++------- src/Umbraco.Web/Editors/UsersController.cs | 29 ++++-- 5 files changed, 121 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index 807d528f3d..a461f62926 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Security { public class EmailService : IIdentityMessageService { - public async Task SendAsync(IdentityMessage message) + public Task SendAsync(IdentityMessage message) { using (var client = new SmtpClient()) using (var mailMessage = new MailMessage()) @@ -19,7 +19,7 @@ namespace Umbraco.Core.Security mailMessage.IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false && message.Body.Contains("<") && message.Body.Contains("(userSave); + + var link = string.Format("{0}#/login/false?invite={1}", + ApplicationContext.UmbracoApplicationUrl, + user.SecurityStamp.ToUrlBase64()); + + try + { + await UserManager.EmailService.SendAsync(new IdentityMessage + { + Body = string.Format("You have been invited to the Umbraco Back Office!\n\nClick this link to accept the invite\n\n{0}", link), + Destination = userSave.Email, + Subject = "You have been invited to the Umbraco Back Office!" + }); + } + catch (Exception ex) + { + throw new HttpResponseException( + Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex)); + } + + //Email was successful, so save the user now Services.UserService.Save(user); - //TODO: Send an email! - await UserManager.EmailService.SendAsync(new IdentityMessage - { - Body = "You have been invited to the Umbraco Back Office!", - Destination = userSave., - Subject = "You have been invited to the Umbraco Back Office!" - }); - return Mapper.Map(user); } From 6cd52bbc3c1574c1f3813efd4c660dcdb577c186 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 May 2017 15:55:36 +0200 Subject: [PATCH 113/714] Gets user invite generating the token, sending the email and then verifying the token --- .../Persistence/Mappers/UserMapper.cs | 1 + src/Umbraco.Core/Services/IUserService.cs | 9 ++- src/Umbraco.Core/Services/UserService.cs | 24 ++++++ .../src/common/resources/auth.resource.js | 74 ++++++++++++------- .../views/common/dialogs/login.controller.js | 54 ++++++++------ .../src/views/common/dialogs/login.html | 6 +- .../Editors/AuthenticationController.cs | 23 +++++- .../Editors/MediaTypeController.cs | 2 +- .../Models/Mapping/UserModelMapper.cs | 2 + 9 files changed, 137 insertions(+), 58 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs index 06e906143a..9d30f571b4 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs @@ -43,6 +43,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.LastLockoutDate, dto => dto.LastLockoutDate); CacheMap(src => src.LastLoginDate, dto => dto.LastLoginDate); CacheMap(src => src.LastPasswordChangeDate, dto => dto.LastPasswordChangeDate); + CacheMap(src => src.SecurityStamp, dto => dto.SecurityStampToken); } #endregion diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 6b798862b0..8aaa35f92e 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -9,7 +9,14 @@ namespace Umbraco.Core.Services /// Defines the UserService, which is an easy access to operations involving and eventually Users. /// public interface IUserService : IMembershipUserService - { + { + /// + /// Checks if a valid token is specified for an invited user and if so returns the user object + /// + /// + /// + IUser ValidateInviteToken(string token); + IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState? userState = null, string[] userGroups = null, string filter = ""); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index dd81824f6a..e542195a01 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -489,6 +489,30 @@ namespace Umbraco.Core.Services } } + public IUser ValidateInviteToken(string token) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateUserRepository(uow); + var query = new Query(); + + query.Where(member => member.SecurityStamp == token); + + var found = repository.GetByQuery(query).ToArray(); + + if (found.Length == 0) return null; + + var user = found[0]; + + //they must not be approved for an invite to work + if (user.IsApproved) return null; + //they should have never logged in for an invite to work + if (user.LastLoginDate != default(DateTime)) return null; + + return user; + } + } + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState? userState = null, string[] userGroups = null, string filter = "") { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 40f1dbb807..d1e9c59b80 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -14,7 +14,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { return { get2FAProviders: function () { - + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( @@ -69,7 +69,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { * */ performLogin: function (username, password) { - + if (!username || !password) { return angularHelper.rejectedPromise({ errorMsg: 'Username or password cannot be empty' @@ -81,12 +81,30 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostLogin"), { - username: username, - password: password - }), + username: username, + password: password + }), 'Login failed for user ' + username); }, + verifyInvite: function (token) { + + if (!token) { + return angularHelper.rejectedPromise({ + errorMsg: 'Token cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostVerifyInvite", { + token: token + })), + 'Failed to verify token ' + token); + }, + /** * @ngdoc method * @name umbraco.resources.authResource#performRequestPasswordReset @@ -114,7 +132,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { errorMsg: 'Email address cannot be empty' }); } - + //TODO: This validation shouldn't really be done here, the validation on the login dialog // is pretty hacky which is why this is here, ideally validation on the login dialog would // be done properly. @@ -130,11 +148,11 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostRequestPasswordReset"), { - email: email - }), + email: email + }), 'Request password reset failed for email ' + email); }, - + /** * @ngdoc method * @name umbraco.resources.authResource#performValidatePasswordResetCode @@ -173,11 +191,11 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { $http.post( umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", - "PostValidatePasswordResetCode"), - { - userId: userId, - resetCode: resetCode - }), + "PostValidatePasswordResetCode"), + { + userId: userId, + resetCode: resetCode + }), 'Password reset code validation failed for userId ' + userId + ', code' + resetCode); }, @@ -234,11 +252,11 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostSetPassword"), - { - userId: userId, - password: password, - resetCode: resetCode - }), + { + userId: userId, + password: password, + resetCode: resetCode + }), 'Password reset code validation failed for userId ' + userId); }, @@ -254,9 +272,9 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostUnLinkLogin"), { - loginProvider: loginProvider, - providerKey: providerKey - }), + loginProvider: loginProvider, + providerKey: providerKey + }), 'Unlinking login provider failed'); }, @@ -278,14 +296,14 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { * @returns {Promise} resourcePromise object * */ - performLogout: function() { + performLogout: function () { return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostLogout"))); }, - + /** * @ngdoc method * @name umbraco.resources.authResource#getCurrentUser @@ -305,13 +323,13 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { * */ getCurrentUser: function () { - + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "GetCurrentUser")), - 'Server call failed for getting current user'); + 'Server call failed for getting current user'); }, getCurrentUserLinkedLogins: function () { @@ -323,7 +341,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { "GetCurrentUserLinkedLogins")), 'Server call failed for getting current users linked logins'); }, - + /** * @ngdoc method * @name umbraco.resources.authResource#isAuthenticated @@ -357,7 +375,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { } return data; }, - error: function (data, status, headers, config) { + error: function (data, status, headers, config) { return { errorMsg: 'Server call failed for checking authentication', data: data, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 106e748c20..c925598bcd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -1,23 +1,29 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", function ($scope, $cookies, $location, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService) { - $scope.isInvite = false; + $scope.invitedUser = null; function init() { // Check if it is a new user if ($location.search().invite) { - $scope.isInvite = true; - $scope.inviteSetPassword = true; + var token = $location.search().invite; + authResource.verifyInvite(token).then(function (data) { + $scope.invitedUser = data; + $scope.inviteSetPassword = true; + }, function () { + //it failed so we should remove the search + $location.search('invite', null); + }); } } - $scope.inviteSavePassword = function() { + $scope.inviteSavePassword = function () { $scope.inviteSetPassword = false; $scope.inviteSetAvatar = true; }; - var setFieldFocus = function(form, field) { - $timeout(function() { + var setFieldFocus = function (form, field) { + $timeout(function () { $("form[name='" + form + "'] input[name='" + field + "']").focus(); }); } @@ -112,7 +118,7 @@ } $scope.loginSubmit = function (login, password) { - + //if the login and password are not empty we need to automatically // validate them - this is because if there are validation errors on the server // then the user has to change both username & password to resubmit which isn't ideal, @@ -127,24 +133,24 @@ } userService.authenticate(login, password) - .then(function(data) { - $scope.submit(true); - }, - function(reason) { + .then(function (data) { + $scope.submit(true); + }, + function (reason) { - //is Two Factor required? - if (reason.status === 402) { - $scope.errorMsg = "Additional authentication required"; - show2FALoginDialog(reason.data.twoFactorView, $scope.submit); - } - else { - $scope.errorMsg = reason.errorMsg; + //is Two Factor required? + if (reason.status === 402) { + $scope.errorMsg = "Additional authentication required"; + show2FALoginDialog(reason.data.twoFactorView, $scope.submit); + } + else { + $scope.errorMsg = reason.errorMsg; - //set the form inputs to invalid - $scope.loginForm.username.$setValidity("auth", false); - $scope.loginForm.password.$setValidity("auth", false); - } - }); + //set the form inputs to invalid + $scope.loginForm.username.$setValidity("auth", false); + $scope.loginForm.password.$setValidity("auth", false); + } + }); //setup a watch for both of the model values changing, if they change // while the form is invalid, then revalidate them so that the form can @@ -166,7 +172,7 @@ if (email && email.length > 0) { $scope.requestPasswordResetForm.email.$setValidity('auth', true); } - + $scope.showEmailResetConfirmation = false; if ($scope.requestPasswordResetForm.$invalid) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 991b52cc62..49b4cadc0d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -8,10 +8,10 @@ - public class ExternalSignInAutoLinkOptions { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying user groups instead")] public ExternalSignInAutoLinkOptions( bool autoLinkExternalAccount = false, string defaultUserType = "editor", @@ -20,26 +23,54 @@ namespace Umbraco.Web.Security.Identity { Mandate.ParameterNotNullOrEmpty(defaultUserType, "defaultUserType"); - _defaultUserType = defaultUserType; + _defaultUserGroups = new[] {defaultUserType}; _defaultAllowedSections = defaultAllowedSections ?? new[] { "content", "media" }; _autoLinkExternalAccount = autoLinkExternalAccount; _defaultCulture = defaultCulture ?? GlobalSettings.DefaultUILanguage; } - //TODO: Change this - it will be different when we have user groups! - private readonly string _defaultUserType; + /// + /// Creates a new instance + /// + /// + /// If null, the default will be the 'editor' group + /// If null the default will the 'content' and 'media' section + /// + public ExternalSignInAutoLinkOptions( + bool autoLinkExternalAccount = false, + string[] defaultUserGroups = null, + string[] defaultAllowedSections = null, + string defaultCulture = null) + { + _defaultUserGroups = defaultUserGroups ?? new[] { "editor" }; + _defaultAllowedSections = defaultAllowedSections ?? new[] { "content", "media" }; + _autoLinkExternalAccount = autoLinkExternalAccount; + _defaultCulture = defaultCulture ?? GlobalSettings.DefaultUILanguage; + } + + private readonly string[] _defaultUserGroups; /// /// A callback executed during account auto-linking and before the user is persisted /// public Action OnAutoLinking { get; set; } - /// - /// The default User Type alias to use for auto-linking users - /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying user groups instead")] public string GetDefaultUserType(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) { - return _defaultUserType; + return _defaultUserGroups.Length == 0 ? "editor" : _defaultUserGroups[0]; + } + + /// + /// The default User group aliases to use for auto-linking users + /// + /// + /// + /// + public string[] GetDefaultUserGroups(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _defaultUserGroups; } private readonly string[] _defaultAllowedSections; From 3a17002ec4696c404d2fc0f7dda32713df93256c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 30 Jun 2017 17:21:40 +1000 Subject: [PATCH 334/714] fixes build after merge --- src/Umbraco.Core/Models/UserExtensions.cs | 16 ++++++++-------- src/Umbraco.Core/Services/UserService.cs | 8 +------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 9c2931ad73..58a844ddf1 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -27,7 +27,7 @@ namespace Umbraco.Core.Models if (user.Avatar.IsNullOrWhiteSpace()) { var gravatarHash = user.Email.ToMd5(); - var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; + var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; //try gravatar var gravatarAccess = staticCache.GetCacheItem("UserAvatar" + user.Id, () => @@ -61,8 +61,8 @@ namespace Umbraco.Core.Models } return null; - } - + } + //use the custom avatar var avatarUrl = FileSystemProviderManager.Current.MediaFileSystem.GetUrl(user.Avatar); return new[] @@ -74,7 +74,7 @@ namespace Umbraco.Core.Models avatarUrl + "?width=300&height=300&mode=crop" }; - } + } /// /// Returns the culture info associated with this user, based on the language they're assigned to in the back office @@ -122,7 +122,7 @@ namespace Umbraco.Core.Models internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); var formattedPath = "," + path + ","; var formattedRecycleBinId = "," + recycleBinId.ToInvariantString() + ","; @@ -130,14 +130,14 @@ namespace Umbraco.Core.Models //check for root path access //TODO: This logic may change if (startNodeIds.Length == 0 || startNodeIds.Contains(Constants.System.Root)) - return true; + return true; //only users with root access have access to the recycle bin so if the above check didn't pass than access is denied if (formattedPath.Contains(formattedRecycleBinId)) { return false; - } - + } + //check for normal paths foreach (var startNodeId in startNodeIds) { diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index f0d2183bfb..ab4da10e6c 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -85,13 +85,7 @@ namespace Umbraco.Core.Services /// IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) { - var userType = GetUserTypeByAlias(memberTypeAlias); - if (userType == null) - { - throw new EntityNotFoundException("The user type " + memberTypeAlias + " could not be resolved"); - } - - return CreateUserWithIdentity(username, email, passwordValue, userType, isApproved); + return CreateUserWithIdentity(username, email, passwordValue, isApproved); } /// From 8fe904cea7adec1e260c0cd31211064e501eed14 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 30 Jun 2017 17:24:48 +1000 Subject: [PATCH 335/714] changing spaces back to 4, finally in sync with mads --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index e99299d699..208052cbb7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ root=true [*] end_of_line = lf indent_style = space -indent_size = 2 +indent_size = 4 trim_trailing_whitespace = true [*.{cs,cshtml,csx,vb,vbx,vbhtml,fs,fsx,txt,ps1,sql}] From e4edd8f6920599b94220634d0907081335535a9f Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 30 Jun 2017 18:14:44 +1000 Subject: [PATCH 336/714] U4-10053 Review anywhere else in the codebase that may reference User Type This also removes some totally unused and ancient code --- .../UmbracoSettings/HelpElement.cs | 6 +- .../UmbracoSettings/IHelpSection.cs | 8 +- .../Configuration/UmbracoSettings/ILink.cs | 7 +- .../IUmbracoSettingsSection.cs | 7 +- .../UmbracoSettings/LinkElement.cs | 6 +- .../UmbracoSettings/LinksCollection.cs | 6 +- .../UmbracoSettings/UmbracoSettingsSection.cs | 3 + .../Models/Membership/UserGroup.cs | 2 +- .../Repositories/UserRepository.cs | 1 - src/Umbraco.Core/Services/UserService.cs | 9 +- .../HelpElementDefaultTests.cs | 20 ---- .../UmbracoSettings/HelpElementTests.cs | 31 ------ .../UmbracoSettings/umbracoSettings.config | 7 -- .../Repositories/UserGroupRepositoryTest.cs | 42 ++++---- .../TestHelpers/SettingsForTests.cs | 4 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - .../views/common/dialogs/help.controller.js | 1 - .../common/overlays/help/help.controller.js | 1 - src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 - src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml | 2 - src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml | 2 - .../Umbraco/config/lang/zh_tw.xml | 2 - .../config/umbracoSettings.config | 6 -- src/Umbraco.Web.UI/umbraco/config/lang/da.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/de.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 - .../umbraco/config/lang/en_us.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/es.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/fr.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/he.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/it.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/ja.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/ko.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/nl.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/pl.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/pt.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/ru.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/sv.xml | 2 - src/Umbraco.Web.UI/umbraco/config/lang/zh.xml | 2 - src/Umbraco.Web.UI/umbraco/helpRedirect.aspx | 12 --- .../Application/UmbracoApplicationActions.js | 14 --- .../Cache/CacheRefresherEventHandler.cs | 2 +- .../Providers/MembersMembershipProvider.cs | 4 +- .../Providers/UsersMembershipProvider.cs | 21 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 4 - .../umbraco.presentation/umbraco/Help.cs | 101 ------------------ .../umbraco/helpRedirect.aspx.cs | 28 ----- .../BasePages/BasePage.cs | 1 - src/umbraco.businesslogic/UmbracoSettings.cs | 2 +- .../UsersMembershipProvider.cs | 1 - .../members/UmbracoMembershipProvider.cs | 2 +- 51 files changed, 86 insertions(+), 313 deletions(-) delete mode 100644 src/Umbraco.Tests/Configurations/UmbracoSettings/HelpElementDefaultTests.cs delete mode 100644 src/Umbraco.Tests/Configurations/UmbracoSettings/HelpElementTests.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/helpRedirect.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/Help.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/helpRedirect.aspx.cs diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/HelpElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/HelpElement.cs index cc4d459359..eb1e452100 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/HelpElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/HelpElement.cs @@ -1,8 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed in future versions")] internal class HelpElement : ConfigurationElement, IHelpSection { [ConfigurationProperty("defaultUrl", DefaultValue = "http://our.umbraco.org/wiki/umbraco-help/{0}/{1}")] diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IHelpSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IHelpSection.cs index 61be2dfaf2..dc9b6af437 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IHelpSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IHelpSection.cs @@ -1,7 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.ComponentModel; namespace Umbraco.Core.Configuration.UmbracoSettings -{ +{ + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed in future versions")] public interface IHelpSection : IUmbracoConfigurationSection { string DefaultUrl { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ILink.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ILink.cs index d2afec55f3..689878cf0d 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ILink.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ILink.cs @@ -1,5 +1,10 @@ -namespace Umbraco.Core.Configuration.UmbracoSettings +using System; +using System.ComponentModel; + +namespace Umbraco.Core.Configuration.UmbracoSettings { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed in future versions")] public interface ILink { string Application { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs index 899de7d1f9..cd4db216ab 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Core.Configuration.UmbracoSettings +using System; +using System.ComponentModel; + +namespace Umbraco.Core.Configuration.UmbracoSettings { public interface IUmbracoSettingsSection : IUmbracoConfigurationSection { @@ -24,6 +27,8 @@ IProvidersSection Providers { get; } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed in future versions")] IHelpSection Help { get; } IWebRoutingSection WebRouting { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/LinkElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/LinkElement.cs index 31b4aa3e93..ad1655dfb0 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/LinkElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/LinkElement.cs @@ -1,7 +1,11 @@ -using System.Configuration; +using System; +using System.ComponentModel; +using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed in future versions")] internal class LinkElement : ConfigurationElement, ILink { [ConfigurationProperty("application")] diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/LinksCollection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/LinksCollection.cs index 5c317790cb..485c8e4bbd 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/LinksCollection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/LinksCollection.cs @@ -1,8 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed in future versions")] internal class LinksCollection : ConfigurationElementCollection, IEnumerable { protected override ConfigurationElement CreateNewElement() diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs index dd6fed5cd5..2c4751f580 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Configuration; using System.Linq; @@ -183,6 +184,8 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return Providers; } } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed in future versions")] IHelpSection IUmbracoSettingsSection.Help { get { return Help; } diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index f48a7b6275..29afc86fc8 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -108,7 +108,7 @@ namespace Umbraco.Core.Models.Membership } /// - /// The set of default permissions for the user type + /// The set of default permissions for the user group /// /// /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index e259eddc6d..bdc0ddeac0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -280,7 +280,6 @@ SELECT 'CountOfInvited' AS name, COUNT(id) AS num FROM umbracoUser WHERE lastLog { {"userDisabled", "IsApproved"}, {"userNoConsole", "IsLockedOut"}, - {"userType", "UserType"}, {"startStructureID", "StartContentId"}, {"startMediaID", "StartMediaId"}, {"userName", "Name"}, diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index ab4da10e6c..d8a3d4fb06 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -365,11 +365,14 @@ namespace Umbraco.Core.Services } - + + /// + /// This is just the default user group that the membership provider will use + /// + /// public string GetDefaultMemberType() { - // User types now being removed, there is no default user type to return - return "None"; + return "writer"; } /// diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/HelpElementDefaultTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/HelpElementDefaultTests.cs deleted file mode 100644 index 7f4e6481de..0000000000 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/HelpElementDefaultTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Linq; -using NUnit.Framework; - -namespace Umbraco.Tests.Configurations.UmbracoSettings -{ - [TestFixture] - public class HelpElementDefaultTests : HelpElementTests - { - protected override bool TestingDefaults - { - get { return true; } - } - - [Test] - public override void Links() - { - Assert.IsTrue(SettingsSection.Help.Links.Count() == 0); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/HelpElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/HelpElementTests.cs deleted file mode 100644 index ba43db8cf0..0000000000 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/HelpElementTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Linq; -using NUnit.Framework; - -namespace Umbraco.Tests.Configurations.UmbracoSettings -{ - [TestFixture] - public class HelpElementTests : UmbracoSettingsTests - { - [Test] - public void DefaultUrl() - { - Assert.IsTrue(SettingsSection.Help.DefaultUrl == "http://our.umbraco.org/wiki/umbraco-help/{0}/{1}"); - } - - [Test] - public virtual void Links() - { - Assert.IsTrue(SettingsSection.Help.Links.Count() == 2); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(0).Application == "content"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(0).ApplicationUrl == "dashboard.aspx"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(0).Language == "en"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(0).UserType == "Administrators"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(0).HelpUrl == "http://www.xyz.no?{0}/{1}/{2}/{3}"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(1).Application == "media"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(1).ApplicationUrl == "dashboard2.aspx"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(1).Language == "ch"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(1).UserType == "Writers"); - Assert.IsTrue(SettingsSection.Help.Links.ElementAt(1).HelpUrl == "http://www.abc.no?{0}/{1}/{2}/{3}"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index a6f9826492..a89a6e4d71 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -237,13 +237,6 @@ - - - - - - - - - - - - - - - + + diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index b237e957a3..5ba3254549 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -24,11 +24,8 @@ - - - - - + + @@ -42,6 +39,5 @@ - - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml index 26aaddb78f..f88ba54d90 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml @@ -43,13 +43,6 @@ - -
User
- /create/simple.ascx - - - -
membergroup
/create/simple.ascx @@ -57,6 +50,13 @@
+ +
membergroup
+ /create/simple.ascx + + + +
Stylesheet
/create/simple.ascx @@ -71,27 +71,6 @@
- -
member
- /create/member.ascx - - - -
- -
member
- /create/member.ascx - - - -
- -
membergroup
- /create/simple.ascx - - - -
Stylesheet editor egenskab
/create/simple.ascx @@ -219,14 +198,6 @@
- -
User Types
- /create/simple.ascx - - - - -
Macro
/Create/PartialView.ascx @@ -235,28 +206,5 @@
- -
Macro
- /Create/PartialViewMacro.ascx - - - - -
- -
Macro
- /Create/PartialView.ascx - - - - -
- -
Macro
- /Create/PartialViewMacro.ascx - - - - -
+ diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml index 265c5b5642..30d97d3991 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml @@ -42,21 +42,6 @@ - - -
User
- /create/user.ascx - - - - -
- -
User
- /create/simple.ascx - - -
membergroup
@@ -221,28 +206,5 @@
- -
Macro
- /Create/PartialViewMacro.ascx - - - - -
- -
Macro
- /Create/PartialView.ascx - - - - -
- -
Macro
- /Create/PartialViewMacro.ascx - - - - -
+ diff --git a/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx b/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx deleted file mode 100644 index dc202b6642..0000000000 --- a/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx +++ /dev/null @@ -1,14 +0,0 @@ -<%@ Register Namespace="umbraco" TagPrefix="umb" Assembly="umbraco" %> -<%@ Page Language="c#" Codebehind="EditUser.aspx.cs" MasterPageFile="../masterpages/umbracoPage.Master" AutoEventWireup="True" Inherits="Umbraco.Web.UI.Umbraco.Users.EditUser" %> -<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> - -<%@ Import Namespace="umbraco.cms.presentation.Trees" %> - - - - - diff --git a/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx.cs deleted file mode 100644 index aa8c0c5b57..0000000000 --- a/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Umbraco.Web.UI.Umbraco.Users -{ - public partial class EditUser : global::umbraco.cms.presentation.user.EditUser - { - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx.designer.cs b/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx.designer.cs deleted file mode 100644 index d9ef0b3cc5..0000000000 --- a/src/Umbraco.Web.UI/umbraco/users/EditUser.aspx.designer.cs +++ /dev/null @@ -1,15 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Umbraco.Web.UI.Umbraco.Users { - - - public partial class EditUser { - } -} diff --git a/src/Umbraco.Web.UI/umbraco/users/EditUserGroup.aspx b/src/Umbraco.Web.UI/umbraco/users/EditUserGroup.aspx deleted file mode 100644 index 6626f33eb1..0000000000 --- a/src/Umbraco.Web.UI/umbraco/users/EditUserGroup.aspx +++ /dev/null @@ -1,52 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EditUserGroup.aspx.cs" MasterPageFile="../masterpages/umbracoPage.Master" - Inherits="umbraco.cms.presentation.user.EditUserGroup" %> - -<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - - - - - - - - - - - - - - - - - - - - - -
-
Users not in group:
- -
-
- - -
-
-
Users in group:
- -
-
-
-
-
-
- -
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/users/NodePermissions.ascx b/src/Umbraco.Web.UI/umbraco/users/NodePermissions.ascx deleted file mode 100644 index 64b6717579..0000000000 --- a/src/Umbraco.Web.UI/umbraco/users/NodePermissions.ascx +++ /dev/null @@ -1,35 +0,0 @@ -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="NodePermissions.ascx.cs" Inherits="umbraco.cms.presentation.user.NodePermissions" %> -

- <%=umbraco.ui.Text("user", "permissionSelectedPages")%> -
- -

- - -

- - - - -

-
- - - - -
    - - -
  • - /> - -
  • -
    - -
- -
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx b/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx deleted file mode 100644 index f94c72fbdc..0000000000 --- a/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx +++ /dev/null @@ -1,48 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="../masterpages/umbracoPage.Master" CodeBehind="PermissionEditor.aspx.cs" Inherits="umbraco.cms.presentation.user.PermissionEditor" %> -<%@ Import Namespace="Umbraco.Web" %> -<%@ Register Src="../controls/Tree/TreeControl.ascx" TagName="TreeControl" TagPrefix="umbraco" %> -<%@ Register Src="NodePermissions.ascx" TagName="NodePermissions" TagPrefix="user" %> -<%@ Register TagPrefix="ui" Namespace="umbraco.uicontrols" Assembly="controls" %> -<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> - - - - - - - - - - - -
- -
- -
- -
- - - -
-
- - -
diff --git a/src/Umbraco.Web.UI/umbraco/users/PermissionsEditor.js b/src/Umbraco.Web.UI/umbraco/users/PermissionsEditor.js deleted file mode 100644 index 61a2c716ac..0000000000 --- a/src/Umbraco.Web.UI/umbraco/users/PermissionsEditor.js +++ /dev/null @@ -1,144 +0,0 @@ -/// -/// -/// -/// - -Umbraco.Sys.registerNamespace("Umbraco.Controls"); - -(function($) { - $.fn.PermissionsEditor = function(opts) { - return this.each(function() { - var conf = $.extend({ - groupId: -1, - pPanelSelector: "", - replacePChkBoxSelector: "" - }, opts); - new Umbraco.Controls.PermissionsEditor().init($(this), conf.groupId, $(conf.pPanelSelector), conf.replacePChkBoxSelector); - }); - }; - $.fn.PermissionsEditorAPI = function() { - /// exposes the Permissions Editor API for the selected object - return $(this).data("PermissionsEditor") == null ? null : $(this).data("PermissionsEditor"); - } - Umbraco.Controls.PermissionsEditor = function() { - /// - /// A class to perform the AJAX callback methods for the PermissionsEditor - /// - return { - //private members - _groupId: -1, - _loadingContent: '







', - _pPanel: null, - _tree: null, - _selectedNodes: new Array(), - _chkReplaceSelector: null, - - init: function(tree, gId, pPanel, chkReplaceSelector) { - ///constructor function - this._groupId = parseInt(gId); - this._pPanel = pPanel; - this._tree = tree; - this._chkReplaceSelector = chkReplaceSelector; - - //store the api object - tree.data("PermissionsEditor", this); - - //bind the nodeClicked event - var _this = this; - var api = this._tree.UmbracoTreeAPI(); - api.addEventHandler("nodeClicked", function(e, n) { _this.treeNodeChecked(e, n) }); - }, - - //public methods - treeNodeChecked: function(e, data) { - var vals = ""; - var nodeId = $(data).attr("id"); - this._tree.find("li a.checked").each(function() { - var cNodeId = $(this).parent("li").attr("id"); - //if the check box is not the one thats just been checked, add it - if (cNodeId != nodeId && parseInt(cNodeId) > 0) { - vals += cNodeId + ","; - } - }); - this._selectedNodes = vals.split(","); - this._selectedNodes.pop(); //remove the last one as it will be empty - //add the one that was just checked to the end of the array if - if ($(data).children("a").hasClass("checked")) { - this._selectedNodes.push(nodeId); - } - if (this._selectedNodes.length > 0) { - this._beginShowNodePermissions(this._selectedNodes.join(",")); - this._pPanel.show(); - } - else { - this._pPanel.hide(); - } - }, - setReplaceChild: function(doReplace) { - alert(doReplace); - this._replaceChildren = doReplace; - }, - beginSavePermissions: function() { - - //ensure that there are nodes selected to save permissions against - if (this._selectedNodes.length == 0) { - alert("No nodes have been selected"); - return; - } - else if (!confirm("Permissions will be changed for nodes: " + this._selectedNodes.join(",") + ". Are you sure?")) { - return; - } - - //get the list of checked permissions - var checkedPermissions = ""; - this._pPanel.find("input:checked").each(function() { - checkedPermissions += $(this).val(); - }); - - var replaceChildren = $(this._chkReplaceSelector).is(":checked"); - - this._setUpdateProgress(); - var _this = this; - setTimeout(function() { _this._savePermissions(_this._selectedNodes.join(","), checkedPermissions, replaceChildren); }, 10); - }, - - //private methods - _addItemToSelectedCollection: function(chkbox) { - if (chkbox.checked) - this._selectedNodes.push(chkbox.value); - else { - var joined = this._selectedNodes.join(','); - joined = joined.replace(chkbox.value, ''); - this._selectedNodes = joined.split(','); - this._selectedNodes = ContentTreeControl_ReBuildArray(contentTreeControl_selected); - } - - }, - _beginShowNodePermissions: function(selectedIDs) { - this._setUpdateProgress(); - var _this = this; - setTimeout(function() { _this._showNodePermissions(selectedIDs); }, 10); - //setTimeout("PermissionsEditor._showNodePermissions('" + selectedIDs + "');", 10); - }, - _showNodePermissions: function(selectedIDs) { - var _this = this; - umbraco.cms.presentation.user.PermissionsHandler.GetNodePermissions(this._groupId, selectedIDs, function (r) { _this._showNodePermissionsCallback(r); }); - }, - _showNodePermissionsCallback: function(result) { - this._pPanel.html(result); - }, - _savePermissions: function(nodeIDs, selectedPermissions, replaceChildren) { - var _this = this; - umbraco.cms.presentation.user.PermissionsHandler.SaveNodePermissions(this._groupId, nodeIDs, selectedPermissions, replaceChildren, function (r) { _this._savePermissionsHandler(r) }); - }, - _savePermissionsHandler: function(result) { - if (UmbClientMgr.mainWindow().UmbSpeechBubble != null) - UmbClientMgr.mainWindow().UmbSpeechBubble.ShowMessage("save", "Saved", "Permissions Saved"); - this._pPanel.html(result); - }, - _setUpdateProgress: function() { - this._pPanel.html(this._loadingContent); - } - } - } -})(jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/users/PermissionsHandler.asmx b/src/Umbraco.Web.UI/umbraco/users/PermissionsHandler.asmx deleted file mode 100644 index 274efb0789..0000000000 --- a/src/Umbraco.Web.UI/umbraco/users/PermissionsHandler.asmx +++ /dev/null @@ -1 +0,0 @@ -<%@ WebService Language="C#" CodeBehind="PermissionsHandler.asmx.cs" Class="umbraco.cms.presentation.user.PermissionsHandler" %> diff --git a/src/Umbraco.Web/Trees/UserTreeController.cs b/src/Umbraco.Web/Trees/UserTreeController.cs index 1318934559..8b70be9c95 100644 --- a/src/Umbraco.Web/Trees/UserTreeController.cs +++ b/src/Umbraco.Web/Trees/UserTreeController.cs @@ -1,4 +1,5 @@ using System.Net.Http.Formatting; +using umbraco; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; @@ -7,8 +8,9 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Users)] - [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 3)] + [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 0)] [PluginController("UmbracoTrees")] + [LegacyBaseTree(typeof(loadUsers))] [CoreTree] public class UserTreeController : TreeController { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 640fd51cfb..7728ca9327 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -493,17 +493,6 @@ ASPXCodeBehind - - - EditUserGroup.aspx - ASPXCodeBehind - - - EditUserGroup.aspx - - - ASPXCodeBehind - @@ -774,7 +763,6 @@ ASPXCodeBehind - ASPXCodeBehind @@ -1016,9 +1004,6 @@ AssignDomain2.aspx - - ASPXCodeBehind - ASPXCodeBehind @@ -1191,8 +1176,6 @@ ASPXCodeBehind - - ASPXCodeBehind @@ -1429,7 +1412,6 @@ - FeedProxy.aspx @@ -1872,29 +1854,10 @@ - XmlTree.xsd - - NodePermissions.ascx - ASPXCodeBehind - - - NodePermissions.ascx - - - PermissionEditor.aspx - ASPXCodeBehind - - - PermissionEditor.aspx - - - PermissionsHandler.asmx - - CacheRefresher.asmx @@ -2049,7 +2012,6 @@ ASPXCodeBehind - @@ -2117,9 +2079,6 @@ - - - ASPXCodeBehind diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroupPermissions.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroupPermissions.cs deleted file mode 100644 index 316a16ceea..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroupPermissions.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.HtmlControls; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using umbraco.businesslogic; -using umbraco.interfaces; -using System.Xml; -using System.Collections.Generic; -using System.Text; -using umbraco.BusinessLogic.Actions; -using Umbraco.Core; - -namespace umbraco.cms.presentation.Trees -{ - [Tree(Constants.Applications.Users, "UserGroupPermissions", "User Group Permissions", sortOrder: 3)] - public class UserGroupPermissions : BaseTree - { - - public UserGroupPermissions(string application) : base(application) { } - - /// - /// don't allow any actions on this tree - /// - /// - protected override void CreateAllowedActions(ref List actions) - { - actions.Clear(); - } - - /// - /// no actions should be able to be performed on the parent node except for refresh - /// - /// - protected override void CreateRootNodeActions(ref List actions) - { - actions.Clear(); - actions.Add(ActionRefresh.Instance); - } - - public override void Render(ref XmlTree tree) - { - var userService = ApplicationContext.Current.Services.UserService; - foreach (var group in userService.GetAllUserGroups()) - { - var node = XmlTreeNode.Create(this); - node.NodeID = group.Id.ToString(); - node.Text = group.Name; - node.Action = "javascript:openUserGroupPermissions('" + group.Id.ToString() + "');"; - node.Icon = "icon-users"; - - OnBeforeNodeRender(ref tree, ref node, EventArgs.Empty); - if (node != null) - { - tree.Add(node); - OnAfterNodeRender(ref tree, ref node, EventArgs.Empty); - } - } - } - - protected override void CreateRootNode(ref XmlTreeNode rootNode) - { - rootNode.Text = ui.Text("user", "userGroupPermissions"); - } - - public override void RenderJS(ref StringBuilder Javascript) - { - Javascript.Append( - @" -function openUserGroupPermissions(id) { - UmbClientMgr.contentFrame('users/PermissionEditor.aspx?id=' + id); -} -"); - } - - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs deleted file mode 100644 index 8c96b2069c..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/UserGroups.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using umbraco.BusinessLogic; -using System.Collections.Generic; -using umbraco.businesslogic; -using Umbraco.Core; - -namespace umbraco.cms.presentation.Trees -{ - [Tree(Constants.Applications.Users, "userGroups", "User Groups", sortOrder: 2)] - public class UserGroups : BaseTree - { - - public UserGroups(string application) : base(application) { } - - public override void RenderJS(ref System.Text.StringBuilder Javascript) - { - Javascript.Append( - @" -function openUserGroups(id) { - UmbClientMgr.contentFrame('users/EditUserGroup.aspx?id=' + id); -} -"); - } - - public override void Render(ref XmlTree tree) - { - var userGroups = ApplicationContext.Current.Services.UserService.GetAllUserGroups(); - foreach (var userGroup in userGroups) - { - XmlTreeNode node = XmlTreeNode.Create(this); - node.NodeID = userGroup.Id.ToString(); - node.Action = string.Format("javascript:openUserGroups({0})", userGroup.Id); - node.Icon = "icon-users"; - node.Text = userGroup.Name; - - OnBeforeNodeRender(ref tree, ref node, EventArgs.Empty); - if (node != null) - { - tree.Add(node); - OnAfterNodeRender(ref tree, ref node, EventArgs.Empty); - } - } - } - - protected override void CreateRootNode(ref XmlTreeNode rootNode) - { - rootNode.Text = ui.Text("user", "userGroups"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadContent.cs index 49251f2ebd..a2a8b26dea 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadContent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Configuration; using umbraco.BusinessLogic.Actions; using umbraco.businesslogic; @@ -11,11 +12,8 @@ using Action = umbraco.BusinessLogic.Actions.Action; namespace umbraco { - /// - /// Handles loading the content tree into umbraco's application tree - /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This is no longer used and will be removed from the codebase in the future")] - //[Tree(Constants.Applications.Content, "content", "Content", silent: true)] public class loadContent : BaseContentTree { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs index c749b6b58b..35cf9878a5 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.IO; using System.Text; @@ -31,11 +32,8 @@ using Umbraco.Core; namespace umbraco { - /// - /// Handles loading of all umbraco users into the users application tree - /// - [Tree(Constants.Applications.Users, "users_old", "Users (Legacy)")] - //TODO: Remove this tree when ready + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed from the codebase in the future")] public class loadUsers : BaseTree { public loadUsers(string application) : base(application) { } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewMacrosTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewMacrosTasks.cs deleted file mode 100644 index 9d1926bee4..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewMacrosTasks.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Umbraco.Core.CodeAnnotations; -using umbraco.BusinessLogic; - -namespace umbraco -{ - /// - /// The UI 'tasks' for the create dialog and delete processes - /// - [UmbracoWillObsolete("http://issues.umbraco.org/issue/U4-1373", "This will one day be removed when we overhaul the create process")] - public class PartialViewMacroTasks : PartialViewTasksBase - { - public override string AssignedApp - { - get { return DefaultApps.developer.ToString(); } - } - - protected override bool IsPartialViewMacro - { - get { return true; } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasks.cs deleted file mode 100644 index 326479ee34..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasks.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Umbraco.Core.CodeAnnotations; -using umbraco.BusinessLogic; - -namespace umbraco -{ - /// - /// The UI 'tasks' for the create dialog and delete processes - /// - [UmbracoWillObsolete("http://issues.umbraco.org/issue/U4-1373", "This will one day be removed when we overhaul the create process")] - public class PartialViewTasks : PartialViewTasksBase - { - public override string AssignedApp - { - get { return DefaultApps.settings.ToString(); } - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasksBase.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasksBase.cs deleted file mode 100644 index 9f810c01b8..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasksBase.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.IO; -using System.Web; -using Umbraco.Core.CodeAnnotations; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web; -using Umbraco.Web.UI; -using umbraco.BasePages; -using Umbraco.Core; - -namespace umbraco -{ - /// - /// The base UI 'tasks' for the create dialog and delete processes - /// - [UmbracoWillObsolete("http://issues.umbraco.org/issue/U4-1373", "This will one day be removed when we overhaul the create process")] - public abstract class PartialViewTasksBase : LegacyDialogTask - { - private string _returnUrl = string.Empty; - - public override string ReturnUrl - { - get { return _returnUrl; } - } - - protected virtual string EditViewFile - { - get { return "Settings/Views/EditView.aspx"; } - } - - protected virtual bool IsPartialViewMacro - { - get { return false; } - } - - public override bool PerformSave() - { - var pipesIndex = Alias.IndexOf("|||", StringComparison.Ordinal); - var snippetName = Alias.Substring(0, pipesIndex).Trim(); - var fileName = Alias.Substring(pipesIndex + 3, Alias.Length - pipesIndex - 3); - if (fileName.ToLowerInvariant().EndsWith(".cshtml") == false) - { - fileName += ".cshtml"; - } - - var model = new PartialView(IsPartialViewMacro ? PartialViewType.PartialViewMacro : PartialViewType.PartialView, fileName); - var fileService = (FileService)ApplicationContext.Current.Services.FileService; - var macroService = ApplicationContext.Current.Services.MacroService; - - if (IsPartialViewMacro == false) - { - var attempt = fileService.CreatePartialView(model, snippetName, User.Id); - _returnUrl = string.Format("settings/views/EditView.aspx?treeType=partialViews&file={0}", HttpUtility.UrlEncode(model.Path.TrimStart('/').Replace("\\", "/"))); - return attempt.Success; - } - else - { - - var attempt = fileService.CreatePartialViewMacro(model, /*ParentID == 1,*/ snippetName, User.Id); - // if ParentId = 0 then that means that the "Create macro" checkbox was OFF, so don't try to create an actual macro - // See PartialViewMacro.ascx.cs and PartialView.ascx.cs: SubmitButton_Click - if (attempt && ParentID != 0) - { - //The partial view path to be saved with the macro must be a fully qualified virtual path - var virtualPath = string.Format("{0}/{1}/{2}", SystemDirectories.MvcViews, "MacroPartials", attempt.Result.Path); - macroService.Save(new Macro(attempt.Result.Alias, attempt.Result.Alias) { ScriptPath = virtualPath }); - } - - _returnUrl = string.Format("settings/views/EditView.aspx?treeType=partialViewMacros&file={0}", HttpUtility.UrlEncode(model.Path.TrimStart('/').Replace("\\", "/"))); - return attempt.Success; - } - - } - - public override bool PerformDelete() - { - var fileService = (FileService)ApplicationContext.Current.Services.FileService; - - if (IsPartialViewMacro == false) - { - if (Alias.Contains(".") == false) - { - //there is no extension so we'll assume it's a folder - fileService.DeletePartialViewFolder(Alias.TrimStart('/')); - return true; - } - return fileService.DeletePartialView(Alias.TrimStart('/'), User.Id); - } - - if (Alias.Contains(".") == false) - { - //there is no extension so we'll assume it's a folder - fileService.DeletePartialViewMacroFolder(Alias.TrimStart('/')); - return true; - } - return fileService.DeletePartialViewMacro(Alias.TrimStart('/'), User.Id); - } - - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/userTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/userTasks.cs deleted file mode 100644 index f62915cfb1..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/userTasks.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Data; -using System.Web.Security; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Web.UI; -using umbraco.BusinessLogic; -using Umbraco.Core.Security; -using umbraco.DataLayer; -using umbraco.BasePages; -using Umbraco.Core.IO; -using umbraco.cms.businesslogic.member; - -namespace umbraco -{ - public class userTasks : LegacyDialogTask - { - private string _returnUrl = ""; - - public override string ReturnUrl - { - get { return _returnUrl; } - } - - public override string AssignedApp - { - get { return DefaultApps.users.ToString(); } - } - - public override bool PerformSave() - { - // Hot damn HACK > user is allways UserType with id = 1 = administrator ??? - // temp password deleted by NH - //BusinessLogic.User.MakeNew(Alias, Alias, "", BusinessLogic.UserType.GetUserType(1)); - //return true; - - var provider = MembershipProviderExtensions.GetUsersMembershipProvider(); - - var status = MembershipCreateStatus.ProviderError; - try - { - // Password is auto-generated. They are they required to change the password by editing the user information. - - var password = Membership.GeneratePassword( - provider.MinRequiredPasswordLength, - provider.MinRequiredNonAlphanumericCharacters); - - var parts = Alias.Split(new[] {'|'}, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length != 2) - { - return false; - } - var login = parts[0]; - var email = parts[1]; - - var u = provider.CreateUser( - login, password, email.Trim().ToLower(), "", "", true, null, out status); - - if (u == null) - { - return false; - } - - _returnUrl = string.Format("users/EditUser.aspx?id={0}", u.ProviderUserKey); - - return status == MembershipCreateStatus.Success; - } - catch (Exception ex) - { - LogHelper.Error(string.Format("Failed to create the user. Error from provider: {0}", status.ToString()), ex); - return false; - } - } - - public override bool PerformDelete() - { - var u = User.GetUser(ParentID); - u.disable(); - return true; - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs index 39216fcdf0..9aed597702 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs @@ -9,7 +9,6 @@ using Umbraco.Core.IO; using Umbraco.Core.Models; using umbraco.BasePages; using System.Linq; -using umbraco.cms.presentation.user; using umbraco.interfaces; using Umbraco.Web; using Umbraco.Core; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs deleted file mode 100644 index 08aff1b605..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ /dev/null @@ -1,637 +0,0 @@ -using System; -using System.Collections; -using System.Configuration.Provider; -using System.Globalization; -using System.Linq; -using System.Linq; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.HtmlControls; -using System.Web.UI.WebControls; -using Umbraco.Core.Logging; -using Umbraco.Web; -using Umbraco.Web.Security; -using umbraco.BasePages; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic.web; -using umbraco.controls; -using umbraco.presentation.channels.businesslogic; -using umbraco.uicontrols; -using umbraco.cms.presentation.Trees; -using Umbraco.Core.IO; -using Umbraco.Core; -using Umbraco.Core.Models; -using PropertyType = umbraco.cms.businesslogic.propertytype.PropertyType; -using System.Text.RegularExpressions; -using System.Text; -using Umbraco.Core.Security; - -namespace umbraco.cms.presentation.user -{ - /// - /// Summary description for EditUser. - /// - public partial class EditUser : EditUserGroupsBase - { - public EditUser() - { - CurrentApp = DefaultApps.users.ToString(); - } - protected HtmlTable macroProperties; - protected TextBox uname = new TextBox() { ID = "uname" }; - protected RequiredFieldValidator unameValidator = new RequiredFieldValidator(); - protected TextBox lname = new TextBox() { ID = "lname" }; - protected RequiredFieldValidator lnameValidator = new RequiredFieldValidator(); - protected CustomValidator lnameCustomValidator = new CustomValidator(); - protected PlaceHolder passw = new PlaceHolder(); - protected TextBox email = new TextBox() { ID = "email" }; - protected RequiredFieldValidator emailValidator = new RequiredFieldValidator(); - protected CustomValidator emailCustomValidator = new CustomValidator(); - protected DropDownList userLanguage = new DropDownList(); - protected CheckBox NoConsole = new CheckBox(); - protected CheckBox Disabled = new CheckBox(); - - protected ContentPicker mediaPicker = new ContentPicker(); - protected ContentPicker contentPicker = new ContentPicker(); - - protected TextBox cName = new TextBox(); - protected CheckBox cFulltree = new CheckBox(); - protected DropDownList cDocumentType = new DropDownList(); - protected DropDownList cDescription = new DropDownList(); - protected DropDownList cCategories = new DropDownList(); - protected DropDownList cExcerpt = new DropDownList(); - protected ContentPicker cMediaPicker = new ContentPicker(); - protected ContentPicker cContentPicker = new ContentPicker(); - //protected CustomValidator sectionValidator = new CustomValidator(); - - protected UpdatePanel pnlGroups = new UpdatePanel(); - protected PlaceHolder pnlGroupControls = new PlaceHolder(); - protected ListBox lstInGroups = new ListBox(); - protected ListBox lstNotInGroups = new ListBox(); - protected Button btnAddGroup = new Button(); - protected Button btnRemoveGroup = new Button(); - - protected Pane pp = new Pane(); - - private User u; - - private MembershipHelper _membershipHelper; - - private MembershipProvider BackOfficeProvider - { - get { return global::Umbraco.Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); } - } - - protected void Page_Load(object sender, EventArgs e) - { - _membershipHelper = new MembershipHelper(UmbracoContext.Current); - int UID = int.Parse(Request.QueryString["id"]); - u = BusinessLogic.User.GetUser(UID); - - //the true admin can only edit the true admin - if (u.Id == 0 && CurrentUser.Id != 0) - { - throw new Exception("Only the root user can edit the 'root' user (id:0)"); - } - - //only another admin can edit another admin (who is not the true admin) - if (u.IsAdmin() && CurrentUser.IsAdmin() == false) - { - throw new Exception("Admin users can only be edited by admins"); - } - - var userCulture = UserExtensions.GetUserCulture(u.Language, Services.TextService); - - // Populate ui language lsit - foreach (var lang in Services.TextService.GetSupportedCultures()) - { - var regionCode = Services.TextService.ConvertToRegionCodeFromSupportedCulture(lang); - - var li = new ListItem(lang.DisplayName, regionCode); - - if (Equals(lang, userCulture)) - li.Selected = true; - - userLanguage.Items.Add(li); - } - - // Console access and disabling - NoConsole.Checked = u.NoConsole; - Disabled.Checked = u.Disabled; - - PlaceHolder medias = new PlaceHolder(); - mediaPicker.AppAlias = Constants.Applications.Media; - mediaPicker.TreeAlias = "media"; - - if (u.StartMediaId > 0) - mediaPicker.Value = u.StartMediaId.ToString(); - else - mediaPicker.Value = "-1"; - - medias.Controls.Add(mediaPicker); - - PlaceHolder content = new PlaceHolder(); - contentPicker.AppAlias = Constants.Applications.Content; - contentPicker.TreeAlias = "content"; - - if (u.StartNodeId > 0) - contentPicker.Value = u.StartNodeId.ToString(); - else - contentPicker.Value = "-1"; - - content.Controls.Add(contentPicker); - - // Add password changer - var passwordChanger = (passwordChanger)LoadControl(SystemDirectories.Umbraco + "/controls/passwordChanger.ascx"); - passwordChanger.MembershipProviderName = UmbracoSettings.DefaultBackofficeProvider; - - // Add a custom validation message for the password changer - var passwordValidation = new CustomValidator - { - ID = "PasswordChangerValidator" - }; - var validatorContainer = new HtmlGenericControl("div") - { - Visible = false, - EnableViewState = false - }; - validatorContainer.Attributes["class"] = "alert alert-error"; - validatorContainer.Style.Add(HtmlTextWriterStyle.MarginTop, "10px"); - validatorContainer.Style.Add(HtmlTextWriterStyle.Width, "300px"); - var validatorContainer2 = new HtmlGenericControl("p"); - validatorContainer.Controls.Add(validatorContainer2); - validatorContainer2.Controls.Add(passwordValidation); - passw.Controls.Add(passwordChanger); - passw.Controls.Add(validatorContainer); - - var validationSummary = new ValidationSummary - { - ID = "validationSummary", - DisplayMode = ValidationSummaryDisplayMode.BulletList, - CssClass = "error" - }; - - pp.addProperty(validationSummary); - - pp.addProperty(ui.Text("user", "username", UmbracoUser), uname, unameValidator); - pp.addProperty(ui.Text("user", "loginname", UmbracoUser), lname, lnameValidator, lnameCustomValidator); - pp.addProperty(ui.Text("user", "password", UmbracoUser), passw); - - pp.addProperty(ui.Text("general", "email", UmbracoUser), email, emailValidator, emailCustomValidator); - pp.addProperty(ui.Text("user", "language", UmbracoUser), userLanguage); - - // Media / content root nodes - var ppNodes = new Pane(); - ppNodes.addProperty(ui.Text("user", "startnode", UmbracoUser), content); - ppNodes.addProperty(ui.Text("user", "mediastartnode", UmbracoUser), medias); - - // General umbraco access - var ppAccess = new Pane(); - ppAccess.addProperty(ui.Text("user", "noConsole", UmbracoUser), NoConsole); - ppAccess.addProperty(ui.Text("user", "disabled", UmbracoUser), Disabled); - - // Groups - var ppGroups = new Pane(); - lstNotInGroups.SelectionMode = ListSelectionMode.Multiple; - btnAddGroup.Text = "Add"; - btnAddGroup.Click += btnAddToGroup_Click; - btnRemoveGroup.Text = "Remove"; - btnRemoveGroup.Click += btnRemoveFromGroup_Click; - lstInGroups.SelectionMode = ListSelectionMode.Multiple; - pnlGroups.ContentTemplateContainer.Controls.Add(pnlGroupControls); - pnlGroups.Attributes.Add("class", "group-selector"); - - var pnl1 = new Panel(); - pnl1.CssClass = "group-selector-list"; - var pnl1Header = new Panel(); - pnl1Header.Controls.Add(new Literal { Text = "Available groups" }); - pnl1.Controls.Add(pnl1Header); - pnl1.Controls.Add(lstNotInGroups); - - var pnl2 = new Panel(); - pnl2.CssClass = "group-selector-buttons"; - pnl2.Controls.Add(btnAddGroup); - pnl2.Controls.Add(btnRemoveGroup); - - var pnl3 = new Panel(); - pnl3.CssClass = "group-selector-list"; - var pnl3Header = new Panel(); - pnl3Header.Controls.Add(new Literal { Text = "Selected groups" }); - pnl3.Controls.Add(pnl3Header); - pnl3.Controls.Add(lstInGroups); - - pnlGroups.ContentTemplateContainer.Controls.Add(pnl1); - pnlGroups.ContentTemplateContainer.Controls.Add(pnl2); - pnlGroups.ContentTemplateContainer.Controls.Add(pnl3); - BindGroups(); - ppGroups.addProperty(ui.Text("user", "userGroups", UmbracoUser), pnlGroups); - - var userInfo = UserTabs.NewTabPage(u.Name); - - userInfo.Controls.Add(pp); - - userInfo.Controls.Add(ppAccess); - userInfo.Controls.Add(ppNodes); - - userInfo.Controls.Add(ppGroups); - - userInfo.HasMenu = true; - - var save = userInfo.Menu.NewButton(); - save.Click += SaveUser_Click; - save.ID = "save"; - save.ToolTip = ui.Text("save"); - save.Text = ui.Text("save"); - save.ButtonType = MenuButtonType.Primary; - - unameValidator.ControlToValidate = uname.ID; - unameValidator.Display = ValidatorDisplay.Dynamic; - unameValidator.ErrorMessage = ui.Text("defaultdialogs", "requiredField", UmbracoUser); - unameValidator.CssClass = "error"; - unameValidator.Style.Add("color", "red"); - unameValidator.Style.Add("margin-left", "5px"); - unameValidator.Style.Add("line-height", "28px"); - - lnameValidator.ControlToValidate = lname.ID; - lnameValidator.Display = ValidatorDisplay.Dynamic; - lnameValidator.ErrorMessage = ui.Text("defaultdialogs", "requiredField", UmbracoUser); - lnameValidator.CssClass = "error"; - lnameValidator.Style.Add("color", "red"); - lnameValidator.Style.Add("margin-left", "5px"); - lnameValidator.Style.Add("line-height", "28px"); - - lnameCustomValidator.ServerValidate += LnameCustomValidator_OnServerValidate; - lnameCustomValidator.Display = ValidatorDisplay.Dynamic; - lnameCustomValidator.ControlToValidate = lname.ID; - var localizedLname = ui.Text("user", "loginname", UmbracoUser); - lnameCustomValidator.ErrorMessage = ui.Text("errorHandling", "errorExistsWithoutTab", localizedLname, UmbracoUser); - lnameCustomValidator.CssClass = "error"; - lnameCustomValidator.Style.Add("color", "red"); - lnameCustomValidator.Style.Add("margin-left", "5px"); - lnameCustomValidator.Style.Add("line-height", "28px"); - - emailValidator.ControlToValidate = email.ID; - emailValidator.Display = ValidatorDisplay.Dynamic; - emailValidator.ErrorMessage = ui.Text("defaultdialogs", "requiredField", UmbracoUser); - emailValidator.CssClass = "error"; - emailValidator.Style.Add("color", "red"); - emailValidator.Style.Add("margin-left", "5px"); - emailValidator.Style.Add("line-height", "28px"); - - emailCustomValidator.ServerValidate += EmailCustomValidator_OnServerValidate; - emailCustomValidator.Display = ValidatorDisplay.Dynamic; - emailCustomValidator.ControlToValidate = email.ID; - var localizedEmail = ui.Text("general", "email", UmbracoUser); - emailCustomValidator.ErrorMessage = ui.Text("errorHandling", "errorRegExpWithoutTab", localizedEmail, UmbracoUser); - emailCustomValidator.CssClass = "error"; - emailCustomValidator.Style.Add("color", "red"); - emailCustomValidator.Style.Add("margin-left", "5px"); - emailCustomValidator.Style.Add("line-height", "28px"); - - SetupForm(); - SetupChannel(); - - ClientTools - .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) - .SyncTree(UID.ToString(), IsPostBack); - } - - private void LnameCustomValidator_OnServerValidate(object source, ServerValidateEventArgs args) - { - var usersWithLoginName = ApplicationContext.Services.UserService.GetByUsername(lname.Text); - args.IsValid = usersWithLoginName == null || usersWithLoginName.Id == u.Id; - } - - private void EmailCustomValidator_OnServerValidate(object source, ServerValidateEventArgs args) - { - args.IsValid = MembershipProviderBase.IsEmailValid(email.Text.Trim()); - } - - private void BindGroups() - { - var userService = ApplicationContext.Current.Services.UserService; - var allGroups = userService.GetAllUserGroups(); - var groupsForUser = userService.GetUserGroupsByAlias(u.GetGroups()); - - lstInGroups.DataSource = groupsForUser; - lstInGroups.DataValueField = "Alias"; - lstInGroups.DataTextField = "Name"; - lstInGroups.DataBind(); - - lstNotInGroups.DataSource = allGroups - .Where(x => groupsForUser.Select(y => y.Id).Contains(x.Id) == false); - lstNotInGroups.DataValueField = "Alias"; - lstNotInGroups.DataTextField = "Name"; - lstNotInGroups.DataBind(); - } - - protected void btnAddToGroup_Click(object sender, EventArgs e) - { - MoveItems(lstNotInGroups, lstInGroups); - } - - protected void btnRemoveFromGroup_Click(object sender, EventArgs e) - { - MoveItems(lstInGroups, lstNotInGroups); - } - - private void SetupChannel() - { - Channel userChannel; - try - { - userChannel = - new Channel(u.Id); - } - catch - { - userChannel = new Channel(); - } - - // Populate dropdowns - var allContentTypes = Services.ContentTypeService.GetAllContentTypes().ToList(); - foreach (var dt in allContentTypes) - { - cDocumentType.Items.Add(new ListItem(dt.Name, dt.Alias)); - } - - // populate fields - var fields = new ArrayList(); - cDescription.ID = "cDescription"; - cCategories.ID = "cCategories"; - cExcerpt.ID = "cExcerpt"; - cDescription.Items.Add(new ListItem(ui.Text("choose"), "")); - cCategories.Items.Add(new ListItem(ui.Text("choose"), "")); - cExcerpt.Items.Add(new ListItem(ui.Text("choose"), "")); - - foreach (var pt in allContentTypes.SelectMany(x => x.PropertyTypes).OrderBy(x => x.Name)) - { - if (fields.Contains(pt.Alias) == false) - { - cDescription.Items.Add(new ListItem(string.Format("{0} ({1})", pt.Name, pt.Alias), pt.Alias)); - cCategories.Items.Add(new ListItem(string.Format("{0} ({1})", pt.Name, pt.Alias), pt.Alias)); - cExcerpt.Items.Add(new ListItem(string.Format("{0} ({1})", pt.Name, pt.Alias), pt.Alias)); - fields.Add(pt.Alias); - } - } - - // Handle content and media pickers - PlaceHolder medias = new PlaceHolder(); - cMediaPicker.AppAlias = Constants.Applications.Media; - cMediaPicker.TreeAlias = "media"; - - if (userChannel.MediaFolder > 0) - cMediaPicker.Value = userChannel.MediaFolder.ToString(); - else - cMediaPicker.Value = "-1"; - - medias.Controls.Add(cMediaPicker); - - PlaceHolder content = new PlaceHolder(); - cContentPicker.AppAlias = Constants.Applications.Content; - cContentPicker.TreeAlias = "content"; - - if (userChannel.StartNode > 0) - cContentPicker.Value = userChannel.StartNode.ToString(); - else - cContentPicker.Value = "-1"; - - content.Controls.Add(cContentPicker); - - - // Setup the panes - Pane ppInfo = new Pane(); - ppInfo.addProperty(ui.Text("name", UmbracoUser), cName); - ppInfo.addProperty(ui.Text("user", "startnode", UmbracoUser), content); - ppInfo.addProperty(ui.Text("user", "searchAllChildren", UmbracoUser), cFulltree); - ppInfo.addProperty(ui.Text("user", "mediastartnode", UmbracoUser), medias); - - Pane ppFields = new Pane(); - ppFields.addProperty(ui.Text("user", "documentType", UmbracoUser), cDocumentType); - ppFields.addProperty(ui.Text("user", "descriptionField", UmbracoUser), cDescription); - ppFields.addProperty(ui.Text("user", "categoryField", UmbracoUser), cCategories); - ppFields.addProperty(ui.Text("user", "excerptField", UmbracoUser), cExcerpt); - - - TabPage channelInfo = UserTabs.NewTabPage(ui.Text("user", "contentChannel", UmbracoUser)); - - channelInfo.Controls.Add(ppInfo); - channelInfo.Controls.Add(ppFields); - - - if (!IsPostBack) - { - cName.Text = userChannel.Name; - cDescription.SelectedValue = userChannel.FieldDescriptionAlias; - cCategories.SelectedValue = userChannel.FieldCategoriesAlias; - cExcerpt.SelectedValue = userChannel.FieldExcerptAlias; - cDocumentType.SelectedValue = userChannel.DocumentTypeAlias; - cFulltree.Checked = userChannel.FullTree; - } - } - - /// - /// Setups the form. - /// - private void SetupForm() - { - if (!IsPostBack) - { - MembershipUser user = BackOfficeProvider.GetUser(u.LoginName, false); - uname.Text = u.Name; - lname.Text = (user == null) ? u.LoginName : user.UserName; - email.Text = (user == null) ? u.Email : user.Email; - - contentPicker.Value = u.StartNodeId.ToString(CultureInfo.InvariantCulture); - mediaPicker.Value = u.StartMediaId.ToString(CultureInfo.InvariantCulture); - } - } - - protected override void OnPreRender(EventArgs e) - { - base.OnPreRender(e); - - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/CMSNode.asmx")); - // ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/legacyAjaxCalls.asmx")); - - } - - /// - /// This handles changing the password - /// - /// - /// - /// - private void ChangePassword(passwordChanger passwordChangerControl, MembershipUser membershipUser, CustomValidator passwordChangerValidator) - { - if (passwordChangerControl.IsChangingPassword) - { - //SD: not sure why this check is here but must have been for some reason at some point? - if (string.IsNullOrEmpty(passwordChangerControl.ChangingPasswordModel.NewPassword) == false) - { - // make sure password is not empty - if (string.IsNullOrEmpty(u.Password)) u.Password = "default"; - } - - var changePasswordModel = passwordChangerControl.ChangingPasswordModel; - - //now do the actual change - var changePassResult = _membershipHelper.ChangePassword( - membershipUser.UserName, changePasswordModel, BackOfficeProvider); - - if (changePassResult.Success) - { - //if it is successful, we need to show the generated password if there was one, so set - //that back on the control - passwordChangerControl.ChangingPasswordModel.GeneratedPassword = changePassResult.Result.ResetPassword; - } - else - { - passwordChangerValidator.IsValid = false; - passwordChangerValidator.ErrorMessage = changePassResult.Result.ChangeError.ErrorMessage; - passw.Controls[1].Visible = true; - } - - } - } - - /// - /// Handles the Click event of the saveUser control. - /// - /// The source of the event. - /// The instance containing the event data. - private void SaveUser_Click(object sender, EventArgs e) - { - if (base.IsValid) - { - try - { - var membershipUser = BackOfficeProvider.GetUser(u.LoginName, false); - if (membershipUser == null) - { - throw new ProviderException("Could not find user in the membership provider with login name " + u.LoginName); - } - - var passwordChangerControl = (passwordChanger)passw.Controls[0]; - var passwordChangerValidator = (CustomValidator)passw.Controls[1].Controls[0].Controls[0]; - - //perform the changing password logic - ChangePassword(passwordChangerControl, membershipUser, passwordChangerValidator); - - //update the membership provider - UpdateMembershipProvider(membershipUser); - - //update the Umbraco user properties - even though we are updating some of these properties in the membership provider that is - // ok since the membership provider might be storing these details someplace totally different! But we want to keep our UI in sync. - u.Name = uname.Text.Trim(); - u.Language = userLanguage.SelectedValue; - u.Email = email.Text.Trim(); - u.LoginName = lname.Text; - u.Disabled = Disabled.Checked; - u.NoConsole = NoConsole.Checked; - - int startNode; - if (int.TryParse(contentPicker.Value, out startNode) == false) - { - //set to default if nothing is choosen - if (u.StartNodeId > 0) - startNode = u.StartNodeId; - else - startNode = -1; - } - u.StartNodeId = startNode; - - - int mstartNode; - if (int.TryParse(mediaPicker.Value, out mstartNode) == false) - { - //set to default if nothing is choosen - if (u.StartMediaId > 0) - mstartNode = u.StartMediaId; - else - mstartNode = -1; - } - u.StartMediaId = mstartNode; - - - u.ClearGroups(); - foreach (ListItem li in lstInGroups.Items) - { - u.AddGroup(li.Value); - } - - u.Save(); - - // save data - if (cName.Text != "") - { - Channel c; - try - { - c = new Channel(u.Id); - } - catch - { - c = new Channel(); - c.User = u; - } - - c.Name = cName.Text; - c.FullTree = cFulltree.Checked; - c.StartNode = int.Parse(cContentPicker.Value); - c.MediaFolder = int.Parse(cMediaPicker.Value); - c.FieldCategoriesAlias = cCategories.SelectedValue; - c.FieldDescriptionAlias = cDescription.SelectedValue; - c.FieldExcerptAlias = cExcerpt.SelectedValue; - c.DocumentTypeAlias = cDocumentType.SelectedValue; - - // - c.MediaTypeAlias = Constants.Conventions.MediaTypes.Image; // [LK:2013-03-22] This was previously lowercase; unsure if using const will cause an issue. - c.MediaTypeFileProperty = Constants.Conventions.Media.File; - c.ImageSupport = true; - - c.Save(); - - } - - ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "editUserSaved", UmbracoUser), ""); - } - catch (Exception ex) - { - ClientTools.ShowSpeechBubble(speechBubbleIcon.error, ui.Text("speechBubbles", "editUserError", UmbracoUser), ""); - LogHelper.Error("Exception", ex); - } - } - else - { - ClientTools.ShowSpeechBubble(speechBubbleIcon.error, - ui.Text("speechBubbles", "validationFailedHeader", UmbracoUser), - ui.Text("speechBubbles", "validationFailedMessage", UmbracoUser)); - } - } - - private void UpdateMembershipProvider(MembershipUser membershipUser) - { - //SD: This check must be here for some reason but apparently we don't want to try to - // update when the AD provider is active. - if ((BackOfficeProvider is ActiveDirectoryMembershipProvider) == false) - { - var membershipHelper = new MembershipHelper(ApplicationContext, new HttpContextWrapper(Context)); - //set the writable properties that we are editing - membershipHelper.UpdateMember(membershipUser, BackOfficeProvider, - email.Text.Trim(), - Disabled.Checked == false); - } - } - - /// - /// UserTabs control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected TabView UserTabs; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx deleted file mode 100644 index 6626f33eb1..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx +++ /dev/null @@ -1,52 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EditUserGroup.aspx.cs" MasterPageFile="../masterpages/umbracoPage.Master" - Inherits="umbraco.cms.presentation.user.EditUserGroup" %> - -<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - - - - - - - - - - - - - - - - - - - - - -
-
Users not in group:
- -
-
- - -
-
-
Users in group:
- -
-
-
-
-
-
- -
\ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs deleted file mode 100644 index a992c4d77e..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Web.UI.WebControls; -using umbraco.BasePages; -using umbraco.BusinessLogic; -using umbraco.cms.presentation.Trees; -using umbraco.interfaces; -using Umbraco.Core; -using Umbraco.Core.Models.Membership; - -namespace umbraco.cms.presentation.user -{ - public partial class EditUserGroup : EditUserGroupsBase - { - public EditUserGroup() - { - CurrentApp = BusinessLogic.DefaultApps.users.ToString(); - } - - protected void Page_Load(object sender, EventArgs e) - { - pnlUmbraco.Text = umbraco.ui.Text("usergroup", base.getUser()); - - var save = pnlUmbraco.Menu.NewButton(); - save.Click += save_Click; - save.ID = "save"; - save.ToolTip = ui.Text("save"); - save.Text = ui.Text("save"); - - pp_alias.Text = umbraco.ui.Text("alias", base.getUser()); - pp_name.Text = umbraco.ui.Text("name", base.getUser()); - - pp_rights.Text = umbraco.ui.Text("default", base.getUser()) + " " + umbraco.ui.Text("rights", base.getUser()); - pp_sections.Text = umbraco.ui.Text("sections", base.getUser()); - - pp_users.Text = umbraco.ui.Text("users", base.getUser()); - - //ensure we have a query string - if (string.IsNullOrEmpty(Request.QueryString["id"])) - return; - //ensure it is an integer - if (!int.TryParse(Request.QueryString["id"], out m_userGroupID)) - return; - - if (!IsPostBack) - { - BindDetails(); - BindActions(); - BindSections(); - BindUsers(); - ClientTools - .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) - .SyncTree(m_userGroupID.ToString(), false); - } - } - - void save_Click(object sender, EventArgs e) - { - var userGroup = CurrentUserGroup; - userGroup.Name = txtUserGroupName.Text; - string actions = ""; - - foreach (ListItem li in cbl_rights.Items) - { - if (li.Selected) - actions += li.Value; - } - - userGroup.Permissions = actions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); ; - - var userIds = new List(); - foreach (ListItem li in lstUsersInGroup.Items) - { - userIds.Add(int.Parse(li.Value)); - } - - userGroup.ClearAllowedSections(); - foreach (ListItem li in cbl_sections.Items) - { - if (li.Selected) userGroup.AddAllowedSection(li.Value); - } - - Services.UserService.Save(userGroup, userIds.ToArray()); - - ClientTools.ShowSpeechBubble(speechBubbleIcon.save, ui.Text("speechBubbles", "editUserGroupSaved", base.getUser()), ""); - } - - protected List CurrentUserGroupActions - { - get - { - if (m_userGroupActions == null) - m_userGroupActions = umbraco.BusinessLogic.Actions.Action.FromString(string.Join("", CurrentUserGroup.Permissions)); - return m_userGroupActions; - } - } - - protected IUserGroup CurrentUserGroup - { - get - { - if (m_userGroup == null) - m_userGroup = ApplicationContext.Current.Services.UserService.GetUserGroupById(m_userGroupID); - return m_userGroup; - } - } - - private IUserGroup m_userGroup; - private List m_userGroupActions; - private int m_userGroupID; - - private void BindDetails() - { - lblUserGroupAlias.Text = CurrentUserGroup.Alias; - txtUserGroupName.Text = CurrentUserGroup.Name; - hidUserGroupID.Value = CurrentUserGroup.Id.ToString(); - } - - private void BindActions() - { - foreach (IAction ai in global::umbraco.BusinessLogic.Actions.Action.GetPermissionAssignable()) - { - - ListItem li = new ListItem(umbraco.ui.Text(ai.Alias, base.getUser()), ai.Letter.ToString()); - - if (CurrentUserGroupActions.Contains(ai)) - li.Selected = true; - - cbl_rights.Items.Add(li); - } - } - - private void BindSections() - { - string currentUserApps = ";"; - foreach (Application a in CurrentUser.Applications) - currentUserApps += a.alias + ";"; - - var gapps = CurrentUserGroup.AllowedSections; - foreach (Application app in BusinessLogic.Application.getAll()) - { - if (CurrentUser.IsAdmin() || currentUserApps.Contains(";" + app.alias + ";")) - { - ListItem li = new ListItem(ui.Text("sections", app.alias), app.alias); - foreach (var tmp in gapps) - { - if (app.alias == tmp) - li.Selected = true; - } - cbl_sections.Items.Add(li); - } - } - } - - private void BindUsers() - { - var userService = ApplicationContext.Current.Services.UserService; - - lstUsersInGroup.DataSource = userService.GetAllInGroup(CurrentUserGroup.Id); - lstUsersInGroup.DataValueField = "Id"; - lstUsersInGroup.DataTextField = "Name"; - lstUsersInGroup.DataBind(); - - lstUsersNotInGroup.DataSource = userService.GetAllNotInGroup(CurrentUserGroup.Id); - lstUsersNotInGroup.DataValueField = "Id"; - lstUsersNotInGroup.DataTextField = "Name"; - lstUsersNotInGroup.DataBind(); - } - - protected void btnAddToGroup_Click(object sender, EventArgs e) - { - MoveItems(lstUsersNotInGroup, lstUsersInGroup); - } - - protected void btnRemoveFromGroup_Click(object sender, EventArgs e) - { - MoveItems(lstUsersInGroup, lstUsersNotInGroup); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.designer.cs deleted file mode 100644 index 2de0f19ef0..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroup.aspx.designer.cs +++ /dev/null @@ -1,195 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.cms.presentation.user { - - - public partial class EditUserGroup { - - /// - /// pnlUmbraco control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.UmbracoPanel pnlUmbraco; - - /// - /// pnl1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pnl1; - - /// - /// hidUserGroupID control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.HiddenField hidUserGroupID; - - /// - /// pp_name control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_name; - - /// - /// txtUserGroupName control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox txtUserGroupName; - - /// - /// pp_alias control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_alias; - - /// - /// lblUserGroupAlias control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Label lblUserGroupAlias; - - /// - /// pnl2 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pnl2; - - /// - /// pp_rights control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_rights; - - /// - /// cbl_rights control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList cbl_rights; - - /// - /// pnl3 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pnl3; - - /// - /// pp_sections control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_sections; - - /// - /// cbl_sections control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList cbl_sections; - - /// - /// pnl4 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pnl4; - - /// - /// pp_users control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_users; - - /// - /// pnlUsers control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.UpdatePanel pnlUsers; - - /// - /// lstUsersNotInGroup control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.ListBox lstUsersNotInGroup; - - /// - /// btnAddToGroup control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button btnAddToGroup; - - /// - /// btnRemoveFromGroup control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button btnRemoveFromGroup; - - /// - /// lstUsersInGroup control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.ListBox lstUsersInGroup; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroupsBase.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroupsBase.cs deleted file mode 100644 index d5710423cf..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUserGroupsBase.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Web.UI.WebControls; -using umbraco.BasePages; - -namespace umbraco.cms.presentation.user -{ - /// - /// Base class provided common functionality to the web forms that allow for assignment of users to groups - /// - public abstract class EditUserGroupsBase : UmbracoEnsuredPage - { - protected void MoveItems(ListBox from, ListBox to) - { - for (var i = from.Items.Count - 1; i >= 0; i--) - { - if (from.Items[i].Selected) - { - to.Items.Add(from.Items[i]); - var li = from.Items[i]; - from.Items.Remove(li); - } - } - - from.SelectedIndex = -1; - to.SelectedIndex = -1; - SortItems(to); - } - - protected void SortItems(ListBox listBox) - { - // Hat-tip: http://stackoverflow.com/a/3396527 - var list = new List(listBox.Items.Cast()); - list = list.OrderBy(li => li.Text).ToList(); - listBox.Items.Clear(); - listBox.Items.AddRange(list.ToArray()); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx b/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx deleted file mode 100644 index 64b6717579..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx +++ /dev/null @@ -1,35 +0,0 @@ -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="NodePermissions.ascx.cs" Inherits="umbraco.cms.presentation.user.NodePermissions" %> -

- <%=umbraco.ui.Text("user", "permissionSelectedPages")%> -
- -

- - -

- - - - -

-
- - - - -
    - - -
  • - /> - -
  • -
    - -
- -
\ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx.cs deleted file mode 100644 index ddd465c699..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Linq; -using Umbraco.Core.Models.Membership; -using System.Collections.Generic; -using umbraco.BusinessLogic.Actions; -using umbraco.interfaces; -using Umbraco.Core; - -namespace umbraco.cms.presentation.user -{ - using Umbraco.Web; - - /// - /// An object to display the current permissions for a user group and a node. - /// - public partial class NodePermissions : System.Web.UI.UserControl - { - private IUserGroup m_umbracoUserGroup; - private UserGroupPermissions m_userGroupPermissions; - private int[] m_nodeID = { -1 }; - private string m_clientItemChecked = "void(0);"; - private bool m_viewOnly = false; - - protected void Page_Load(object sender, EventArgs e) - { - DataBind(); - } - - public int UserGroupID - { - get { return m_umbracoUserGroup.Id; } - set - { - var userService = ApplicationContext.Current.Services.UserService; - m_umbracoUserGroup = userService.GetUserGroupById(value); - m_userGroupPermissions = new UserGroupPermissions(m_umbracoUserGroup); - } - } - - public bool ViewOnly - { - get { return m_viewOnly; } - set { m_viewOnly = value; } - } - - public int[] NodeID - { - get { return m_nodeID; } - set { m_nodeID = value; } - } - - /// - /// The JavaScript method to call when a node is checked. The method will receive a comma separated list of node IDs that are checked. - /// - public string OnClientItemChecked - { - get { return m_clientItemChecked; } - set { m_clientItemChecked = value; } - } - - public override void DataBind() - { - base.DataBind(); - - if (m_umbracoUserGroup == null) - throw new ArgumentNullException("No user group specified"); - - //lookup permissions for last node selected - var selectedNodeId = m_nodeID[m_nodeID.Length - 1]; - - //get the logged in user's permissions - var userService = ApplicationContext.Current.Services.UserService; - var currUserPermissions = userService.GetPermissions(UmbracoContext.Current.Security.CurrentUser, selectedNodeId).Single(); - - var lstCurrUserActions = BusinessLogic.Actions.Action.FromString(string.Join(string.Empty, currUserPermissions.AssignedPermissions)); - var lstLookupGroupActions = m_userGroupPermissions.GetExistingNodePermission(selectedNodeId); - - var lstAllActions = BusinessLogic.Actions.Action.GetPermissionAssignable(); - - //no node is selected, disable the check boxes - if (NodeID[0] == -1) - { - ShowMessage("No node selected"); - return; - } - - //ensure the current user has access to assign permissions. - if (lstCurrUserActions.Contains(ActionRights.Instance)) - BindExistingPermissions(lstAllActions, lstLookupGroupActions); - else - ShowMessage("You do not have access to assign permissions to this node"); - - var names = string.Empty; - foreach (int id in NodeID) - { - if (id > 0) - { - names += new cms.businesslogic.web.Document(id).Text + ", "; - } - } - - lt_names.Text = names.Trim().Trim(','); - } - - protected void ShowMessage(string msg) - { - lblMessage.Visible = true; - lblMessage.Text = msg; - } - - protected void BindExistingPermissions(List allActions, List userActions) - { - List assignedPermissions = new List(); - foreach (umbraco.interfaces.IAction a in allActions) - { - AssignedPermission p = new AssignedPermission(); - p.Permission = a; - p.HasPermission = (userActions != null ? userActions.Contains(a) : false); - assignedPermissions.Add(p); - } - - rptPermissionsList.DataSource = assignedPermissions; - rptPermissionsList.DataBind(); - } - - protected struct AssignedPermission - { - public IAction Permission; - public bool HasPermission; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx.designer.cs deleted file mode 100644 index c981de682b..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/NodePermissions.ascx.designer.cs +++ /dev/null @@ -1,51 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.cms.presentation.user { - - - public partial class NodePermissions { - - /// - /// lt_names control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal lt_names; - - /// - /// pnlReplaceChildren control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel pnlReplaceChildren; - - /// - /// lblMessage control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Label lblMessage; - - /// - /// rptPermissionsList control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Repeater rptPermissionsList; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx deleted file mode 100644 index 49242d09ca..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx +++ /dev/null @@ -1,47 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="../masterpages/umbracoPage.Master" CodeBehind="PermissionEditor.aspx.cs" Inherits="umbraco.cms.presentation.user.PermissionEditor" %> -<%@ Import Namespace="Umbraco.Web" %> -<%@ Register Src="../controls/Tree/TreeControl.ascx" TagName="TreeControl" TagPrefix="umbraco" %> -<%@ Register Src="NodePermissions.ascx" TagName="NodePermissions" TagPrefix="user" %> -<%@ Register TagPrefix="ui" Namespace="umbraco.uicontrols" Assembly="controls" %> -<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> - - - - - - - - - - - -
- -
- -
- -
- - - -
-
- - -
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx.cs deleted file mode 100644 index 1b417f9d47..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Web.UI; -using umbraco.BasePages; -using umbraco.cms.presentation.Trees; -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Models.Membership; - -namespace umbraco.cms.presentation.user -{ - public partial class PermissionEditor : UmbracoEnsuredPage - { - public PermissionEditor() - { - CurrentApp = BusinessLogic.DefaultApps.users.ToString(); - - } - - protected override void OnInit(EventArgs e) - { - base.OnInit(e); - - if (!IsPostBack) - { - JTree.App = Constants.Applications.Content; - JTree.ShowContextMenu = false; - JTree.IsDialog = true; - } - } - - protected void Page_Load(object sender, EventArgs e) - { - if (string.IsNullOrEmpty(Request.QueryString["id"])) - return; - - CheckUserGroup(Request.QueryString["id"]); - - InitControls(); - InitTree(); - } - - private void InitControls() - { - var save = pnlUmbraco.Menu.NewButton(); - save.ID = "btnSave"; - save.ButtonType = uicontrols.MenuButtonType.Primary; - save.OnClientClick = "SavePermissions(); return false;"; - save.Text = ui.Text("save"); - save.ToolTip = ui.Text("save"); - - pnl1.Text = ui.Text("user", "permissionSelectPages"); - } - - private void InitTree() - { - if (!IsPostBack) - { - ClientTools cTools = new ClientTools(this); - cTools.SetActiveTreeType( - TreeDefinitionCollection.Instance.FindTree().Tree.Alias) - .SyncTree(Request.QueryString["id"], false); - } - } - - /// - /// Makes sure the user group exists with the id specified - /// - /// - private void CheckUserGroup(string strID) - { - int id; - bool parsed = false; - IUserGroup group = null; - if (parsed = int.TryParse(strID, out id)) - { - var userService = ApplicationContext.Current.Services.UserService; - group = userService.GetUserGroupById(id); - } - - if (group == null || parsed == false) - { - throw new Exception("No group found with id: " + strID); - } - - nodePermissions.UserGroupID = id; - } - - - protected override void OnPreRender(EventArgs e) { - base.OnPreRender(e); - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference(SystemDirectories.Umbraco + "/users/PermissionsHandler.asmx")); - } - - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx.designer.cs deleted file mode 100644 index e0c1494dd4..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx.designer.cs +++ /dev/null @@ -1,78 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.cms.presentation.user { - - - public partial class PermissionEditor { - - /// - /// CssInclude2 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.CssInclude CssInclude2; - - /// - /// CssInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.CssInclude CssInclude1; - - /// - /// JsInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude1; - - /// - /// pnlUmbraco control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.UmbracoPanel pnlUmbraco; - - /// - /// pnl1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pnl1; - - /// - /// JTree control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.controls.Tree.TreeControl JTree; - - /// - /// nodePermissions control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.cms.presentation.user.NodePermissions nodePermissions; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx deleted file mode 100644 index 274efb0789..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx +++ /dev/null @@ -1 +0,0 @@ -<%@ WebService Language="C#" CodeBehind="PermissionsHandler.asmx.cs" Class="umbraco.cms.presentation.user.PermissionsHandler" %> diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx.cs deleted file mode 100644 index a570a5c7bc..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionsHandler.asmx.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Web; -using System.Web.Script.Services; -using System.Web.Services; -using System.Web.UI; -using umbraco.BasePages; -using Umbraco.Core; -using Umbraco.Core.IO; - -namespace umbraco.cms.presentation.user -{ - /// - /// Summary description for GroupPermissionsHandler - /// - [WebService(Namespace = "http://tempuri.org/")] - [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] - [ToolboxItem(false)] - [ScriptService] - public class PermissionsHandler - { - /// - /// Loads the NodePermissions UserControl with the appropriate properties, renders the contents and returns the output html. - /// - /// - [WebMethod] - public string GetNodePermissions(int groupId, string nodes) - { - Authorize(); - - var page = new Page(); - - var path = SystemDirectories.Umbraco + "/users/NodePermissions.ascx"; - var nodePermissions = (NodePermissions)page.LoadControl(path); - - nodePermissions.UserGroupID = groupId; - nodePermissions.NodeID = ToIntArray(nodes); - nodePermissions.ID = "nodePermissions"; - - page.Controls.Add(nodePermissions); - return GetPageResult(page); - } - - [WebMethod] - public string SaveNodePermissions(int groupId, string nodes, string permissions, bool replaceChild) - { - Authorize(); - - var userService = ApplicationContext.Current.Services.UserService; - var groupPermissions = new UserGroupPermissions(userService.GetUserGroupById(groupId)); - var actions = umbraco.BusinessLogic.Actions.Action.FromString(permissions); - groupPermissions.SaveNewPermissions(ToIntArray(nodes), actions, replaceChild); - - return GetNodePermissions(groupId, nodes); - } - - private void Authorize() - { - if (!BasePage.ValidateUserContextID(BasePage.umbracoUserContextID)) - { - throw new Exception("Client authorization failed. User is not logged in"); - } - } - - private static string GetPageResult(Page page) - { - var sw = new StringWriter(); - HttpContext.Current.Server.Execute(page, sw, false); - return sw.ToString(); - } - - private int[] ToIntArray(string nodeIds) - { - var s_nodes = nodeIds.Split(','); - var i_nodes = new int[s_nodes.Length]; - - for (int i = 0; i < s_nodes.Length; i++) - { - i_nodes[i] = int.Parse(s_nodes[i]); - } - - return i_nodes; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupPermissions.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupPermissions.cs deleted file mode 100644 index 7eb24ed283..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/UserGroupPermissions.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Data; -using Umbraco.Core; -using Umbraco.Web; -using umbraco.BusinessLogic.Actions; -using umbraco.interfaces; -using umbraco.cms.businesslogic.web; -using Umbraco.Core.Models.Membership; - -namespace umbraco.cms.presentation.user -{ - /// - /// Provides umbraco user group permission functionality on various nodes. Only nodes that are published are queried via the cache. - /// - public class UserGroupPermissions - { - readonly IUserGroup _group; - - public UserGroupPermissions(IUserGroup group) - { - _group = group; - } - - /// - /// saves new permissions with the parameters supplied - /// - /// - /// - /// - public void SaveNewPermissions(int[] nodeIDs, List actions, bool replaceChildren) - { - //ensure permissions that are permission assignable - var permissions = actions.FindAll(a => (a.CanBePermissionAssigned)); - - //ensure that only the nodes that the user has permissions to update are updated - var lstNoPermissions = new List(); - foreach (var nodeId in nodeIDs) - { - var nodeActions = UmbracoContext.Current.UmbracoUser.GetPermissions(GetNodePath(nodeId)); - var lstActions = BusinessLogic.Actions.Action.FromString(nodeActions); - if (lstActions == null || !lstActions.Contains(ActionRights.Instance)) - lstNoPermissions.Add(nodeId); - } - //remove the nodes that the user doesn't have permission to update - var lstNodeIDs = new List(); - lstNodeIDs.AddRange(nodeIDs); - foreach (var noPermission in lstNoPermissions) - lstNodeIDs.Remove(noPermission); - nodeIDs = lstNodeIDs.ToArray(); - - //get the complete list of node ids that this change will affect - var allNodes = new List(); - if (replaceChildren) - { - foreach (var nodeId in nodeIDs) - { - allNodes.Add(nodeId); - allNodes.AddRange(FindChildNodes(nodeId)); - } - } - else - { - allNodes.AddRange(nodeIDs); - } - - //if permissions are to be assigned, then assign them - if (permissions.Count > 0) - { - AssignPermissions(permissions.Select(x => x.Letter), allNodes.ToArray()); - } - else - { - //If there are NO permissions for this node, we need to assign the ActionNull permission otherwise - //the node will inherit from it's parent. - AssignPermissions(new[] { ActionNull.Instance.Letter }, allNodes.ToArray()); - } - } - - protected void AssignPermissions(IEnumerable permissions, params int[] entityIds) - { - var service = ApplicationContext.Current.Services.UserService; - service.ReplaceUserGroupPermissions(_group.Id, permissions, entityIds); - } - - /// - /// Returns the current user permissions for the node specified - /// - /// - /// - public List GetExistingNodePermission(int nodeId) - { - var path = GetNodePath(nodeId); - if (path != "") - { - //get the user and their permissions - string permissions = GetPermissions(path); - return umbraco.BusinessLogic.Actions.Action.FromString(permissions); - } - return null; - } - - private string GetPermissions(string path) - { - var userService = ApplicationContext.Current.Services.UserService; - return userService.GetPermissionsForPath(_group, path); - } - - /// - /// gets path attribute for node id passed - /// - /// - /// - private static string GetNodePath(int iNodeId) - { - if (Document.IsDocument(iNodeId)) - { - var doc = new Document(iNodeId); - return doc.Path; - } - - return ""; - } - - /// - /// Finds all child node IDs - /// - /// - /// - private static IEnumerable FindChildNodes(int nodeId) - { - var docs = Document.GetChildrenForTree(nodeId); - var nodeIds = new List(); - foreach (var doc in docs) - { - nodeIds.Add(doc.Id); - if (doc.HasChildren) - { - nodeIds.AddRange(FindChildNodes(doc.Id)); - } - } - return nodeIds; - } - } -} \ No newline at end of file From 2aaf1048569dfce7f336e0418443a10fba563fd6 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 30 Jun 2017 10:51:23 +0200 Subject: [PATCH 339/714] change text on cancel button to close --- src/Umbraco.Web.UI.Client/src/views/content/rights.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/rights.html b/src/Umbraco.Web.UI.Client/src/views/content/rights.html index c9d4df5dbf..e992fd3e01 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/rights.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/rights.html @@ -54,7 +54,7 @@ -
+
Content start node: {{ startNode.name }} @@ -21,7 +21,7 @@
-
+
Media start node: {{ startNode.name }} From ff02b28d26fdd5cce767b9b2e0c2e160955acbbc Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 3 Jul 2017 16:31:47 +1000 Subject: [PATCH 342/714] Fixes tree for single tree sections, fixes creating a user group so you can select default permissions --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 13 ---------- .../Editors/UserGroupsController.cs | 2 +- .../Models/Mapping/PermissionsResolver.cs | 2 +- src/Umbraco.Web/Security/WebSecurity.cs | 2 +- .../Trees/ApplicationTreeController.cs | 24 +++++++++++++++---- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index fb7222c04f..a4c549ac75 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -573,7 +573,6 @@ - @@ -583,13 +582,6 @@ umbraco.aspx - - EditUser.aspx - ASPXCodeBehind - - - EditUser.aspx - @@ -1011,10 +1003,6 @@ - - - - @@ -1068,7 +1056,6 @@ - diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index 498e792446..78dc0afeda 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -39,7 +39,7 @@ namespace Umbraco.Web.Editors /// public UserGroupDisplay GetEmptyUserGroup() { - return new UserGroupDisplay(); + return Mapper.Map(new UserGroup()); } /// diff --git a/src/Umbraco.Web/Models/Mapping/PermissionsResolver.cs b/src/Umbraco.Web/Models/Mapping/PermissionsResolver.cs index 297b4092cb..d39ddf5998 100644 --- a/src/Umbraco.Web/Models/Mapping/PermissionsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PermissionsResolver.cs @@ -46,7 +46,7 @@ namespace Umbraco.Web.Models.Mapping : attribute.Name; result.Description = _textService.Localize(String.Format("actionDescriptions/{0}", action.Alias)); result.Icon = action.Icon; - result.Checked = source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)); + 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/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 9178215706..e4a29bd6cc 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web.Security public WebSecurity(HttpContextBase httpContext, ApplicationContext applicationContext) { _httpContext = httpContext; - _applicationContext = applicationContext; + _applicationContext = applicationContext; } /// diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index f6b5e82fc3..566d723460 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Trees //find all tree definitions that have the current application alias var appTrees = ApplicationContext.Current.Services.ApplicationTreeService.GetApplicationTrees(application, onlyInitialized).ToArray(); - if (appTrees.Count() == 1 || string.IsNullOrEmpty(tree) == false ) + if (string.IsNullOrEmpty(tree) == false || appTrees.Length == 1) { var apptree = string.IsNullOrEmpty(tree) == false ? appTrees.SingleOrDefault(x => x.Alias == tree) @@ -55,11 +55,12 @@ namespace Umbraco.Web.Trees Constants.System.Root.ToString(CultureInfo.InvariantCulture), queryStrings, application); - - return result; + + //this will be null if it cannot convert to ta single root section + if (result != null) + return result; } - var collection = new TreeNodeCollection(); foreach (var apptree in appTrees) { @@ -107,6 +108,7 @@ namespace Umbraco.Web.Trees /// /// /// + /// /// private async Task GetRootForSingleAppTree(ApplicationTree configTree, string id, FormDataCollection queryStrings, string application) { @@ -121,6 +123,16 @@ namespace Umbraco.Web.Trees //This should really never happen if we've successfully got the children above. throw new InvalidOperationException("Could not create root node for tree " + configTree.Alias); } + + //if the root node has a route path, we cannot create a single root section because by specifying the route path this would + //override the dashboard route and that means there can be no dashboard for that section which is a breaking change. + if (rootNode.Result.RoutePath.IsNullOrWhiteSpace() == false + && rootNode.Result.RoutePath != "#" + && rootNode.Result.RoutePath != application) + { + //null indicates this cannot be converted + return null; + } var sectionRoot = SectionRootNode.CreateSingleTreeSectionRoot( rootId, @@ -128,6 +140,10 @@ namespace Umbraco.Web.Trees rootNode.Result.MenuUrl, rootNode.Result.Name, byControllerAttempt.Result); + + //This can't be done currently because the root will default to routing to a dashboard and if we disable dashboards for a section + //that is really considered a breaking change. See above. + //sectionRoot.RoutePath = rootNode.Result.RoutePath; foreach (var d in rootNode.Result.AdditionalData) { From 94b0cb04da56c9469b6e3eda2ebf1f9ee05e6e18 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 3 Jul 2017 17:58:04 +1000 Subject: [PATCH 343/714] fix tests --- src/Umbraco.Tests/Plugins/PluginManagerTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index aa30c4415a..28987ed356 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -294,7 +294,7 @@ AnotherContentFinder { var trees = _manager.ResolveAttributedTrees(); // commit 6c5e35ec2cbfa31be6790d1228e0c2faf5f55bc8 brings the count down to 14 - Assert.AreEqual(12, trees.Count()); + Assert.AreEqual(9, trees.Count()); } [Test] @@ -308,7 +308,7 @@ AnotherContentFinder public void Resolves_Trees() { var trees = _manager.ResolveTrees(); - Assert.AreEqual(37, trees.Count()); + Assert.AreEqual(35, trees.Count()); } [Test] From 82bd11e66d52ddbf8d0d06db3b0cc17bf73c781f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 3 Jul 2017 19:27:48 +1000 Subject: [PATCH 344/714] Gets filtering on user state working --- .../Repositories/UserRepository.cs | 42 ++- .../src/common/resources/users.resource.js | 335 +++++++++--------- .../users/views/users/users.controller.js | 37 +- .../src/views/users/views/users/users.html | 2 +- src/Umbraco.Web/Editors/UsersController.cs | 9 +- 5 files changed, 242 insertions(+), 183 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index bdc0ddeac0..d6c97b6df2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -79,15 +79,16 @@ namespace Umbraco.Core.Persistence.Repositories public IDictionary GetUserStates() { - var sql = @"SELECT 'CountOfAll' AS name, COUNT(id) AS num FROM umbracoUser + var sql = @"SELECT '1CountOfAll' AS colName, COUNT(id) AS num FROM umbracoUser UNION -SELECT 'CountOfActive' AS name, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL +SELECT '2CountOfActive' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL UNION -SELECT 'CountOfDisabled' AS name, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 1 +SELECT '3CountOfDisabled' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 1 UNION -SELECT 'CountOfLockedOut' AS name, COUNT(id) AS num FROM umbracoUser WHERE userNoConsole = 1 +SELECT '4CountOfLockedOut' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userNoConsole = 1 UNION -SELECT 'CountOfInvited' AS name, COUNT(id) AS num FROM umbracoUser WHERE lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL"; +SELECT '5CountOfInvited' AS colName, COUNT(id) AS num FROM umbracoUser WHERE lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL +ORDER BY colName"; var result = Database.Fetch(sql); @@ -513,7 +514,7 @@ SELECT 'CountOfInvited' AS name, COUNT(id) AS num FROM umbracoUser WHERE lastLog Sql filterSql = null; - if (filter != null || (userGroups != null && userGroups.Length > 0)) + if (filter != null || (userGroups != null && userGroups.Length > 0) || (userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false)) filterSql = new Sql(); if (filter != null) @@ -531,10 +532,33 @@ SELECT 'CountOfInvited' AS name, COUNT(id) AS num FROM umbracoUser WHERE lastLog INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; filterSql.Append(subQuery, new {userGroups= userGroups}); + } + if (userState != null && userState.Length > 0) + { + //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering + if (userState.Contains(UserState.All) == false) + { + if (userState.Contains(UserState.Active)) + { + filterSql.Append("AND (userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); + } + if (userState.Contains(UserState.Disabled)) + { + filterSql.Append("AND (userDisabled = 1)"); + } + if (userState.Contains(UserState.LockedOut)) + { + filterSql.Append("AND (userNoConsole = 1)"); + } + if (userState.Contains(UserState.Invited)) + { + filterSql.Append("AND (lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL)"); + } + } } - - // Get base query for returning IDs - var sqlBaseIds = GetBaseQuery("id"); + + // Get base query for returning IDs + var sqlBaseIds = GetBaseQuery("id"); if (query == null) query = new Query(); var translatorIds = new SqlTranslator(sqlBaseIds, query); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 7631fda31f..706142dd18 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -7,179 +7,184 @@ * Used by the users section to get users and send requests to create, invite, delete, etc. users. */ (function () { - 'use strict'; + 'use strict'; - function usersResource($http, umbRequestHelper, $q, umbDataFormatter) { + function usersResource($http, umbRequestHelper, $q, umbDataFormatter) { - function clearAvatar(userId) { + function clearAvatar(userId) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostClearAvatar", - { id: userId })), - 'Failed to clear the user avatar ' + userId); - } + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostClearAvatar", + { id: userId })), + 'Failed to clear the user avatar ' + userId); + } - function disableUsers(userIds) { - if (!userIds) { - throw "userIds not specified"; - } + function disableUsers(userIds) { + if (!userIds) { + throw "userIds not specified"; + } - //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed - var qry = "userIds=" + userIds.join("&userIds="); + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = "userIds=" + userIds.join("&userIds="); - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostDisableUsers", qry)), - 'Failed to disable the users ' + userIds.join(",")); + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostDisableUsers", qry)), + 'Failed to disable the users ' + userIds.join(",")); + } + + function enableUsers(userIds) { + if (!userIds) { + throw "userIds not specified"; + } + + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = "userIds=" + userIds.join("&userIds="); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostEnableUsers", qry)), + 'Failed to enable the users ' + userIds.join(",")); + } + + function getPagedResults(options) { + var defaults = { + pageSize: 25, + pageNumber: 1, + filter: '', + orderDirection: "Ascending", + orderBy: "Username", + userGroups: [], + userStates: [] + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + var params = { + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + filter: options.filter + }; + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = umbRequestHelper.dictionaryToQueryString(params); + if (options.userGroups.length > 0) { + //we need to create a custom query string for an array + qry += "&userGroups=" + options.userGroups.join("&userGroups="); + } + if (options.userStates.length > 0) { + //we need to create a custom query string for an array + qry += "&userStates=" + options.userStates.join("&userStates="); + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "GetPagedUsers", + qry)), + 'Failed to retrieve users paged result'); + } + + function getUser(userId) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "GetById", + { id: userId })), + "Failed to retrieve data for user " + userId); + } + + function createUser(user) { + if (!user) { + throw "user not specified"; + } + + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserPostData(user); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostCreateUser"), + formattedSaveData), + "Failed to save user"); + } + + function inviteUser(user) { + if (!user) { + throw "user not specified"; + } + + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserPostData(user); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostInviteUser"), + formattedSaveData), + "Failed to invite user"); + } + + function saveUser(user) { + if (!user) { + throw "user not specified"; + } + + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserPostData(user); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostSaveUser"), + formattedSaveData), + "Failed to save user"); + } + + + var resource = { + disableUsers: disableUsers, + enableUsers: enableUsers, + getPagedResults: getPagedResults, + getUser: getUser, + createUser: createUser, + inviteUser: inviteUser, + saveUser: saveUser, + clearAvatar: clearAvatar + }; + + return resource; + } - function enableUsers(userIds) { - if (!userIds) { - throw "userIds not specified"; - } - - //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed - var qry = "userIds=" + userIds.join("&userIds="); - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostEnableUsers", qry)), - 'Failed to enable the users ' + userIds.join(",")); - } - - function getPagedResults(options) { - var defaults = { - pageSize: 25, - pageNumber: 1, - filter: '', - orderDirection: "Ascending", - orderBy: "Username", - userGroups: [] - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - var params = { - pageNumber: options.pageNumber, - pageSize: options.pageSize, - orderBy: options.orderBy, - orderDirection: options.orderDirection, - filter: options.filter - }; - //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed - var qry = umbRequestHelper.dictionaryToQueryString(params); - if (options.userGroups.length > 0) { - //we need to create a custom query string for an array - qry += "&userGroups=" + options.userGroups.join("&userGroups="); - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "GetPagedUsers", - qry)), - 'Failed to retrieve users paged result'); - } - - function getUser(userId) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "GetById", - { id: userId })), - "Failed to retrieve data for user " + userId); - } - - function createUser(user) { - if (!user) { - throw "user not specified"; - } - - //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post - var formattedSaveData = umbDataFormatter.formatUserPostData(user); - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostCreateUser"), - formattedSaveData), - "Failed to save user"); - } - - function inviteUser(user) { - if (!user) { - throw "user not specified"; - } - - //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post - var formattedSaveData = umbDataFormatter.formatUserPostData(user); - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostInviteUser"), - formattedSaveData), - "Failed to invite user"); - } - - function saveUser(user) { - if (!user) { - throw "user not specified"; - } - - //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post - var formattedSaveData = umbDataFormatter.formatUserPostData(user); - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostSaveUser"), - formattedSaveData), - "Failed to save user"); - } - - - var resource = { - disableUsers: disableUsers, - enableUsers: enableUsers, - getPagedResults: getPagedResults, - getUser: getUser, - createUser: createUser, - inviteUser: inviteUser, - saveUser: saveUser, - clearAvatar: clearAvatar - }; - - return resource; - - } - - angular.module('umbraco.resources').factory('usersResource', usersResource); + angular.module('umbraco.resources').factory('usersResource', usersResource); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index ece0fe0fa5..67b4faf236 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -313,7 +313,42 @@ return name; } - function setUserStatesFilter(value) { + function setUserStatesFilter(userState) { + + if (!vm.usersOptions.userStates) { + vm.usersOptions.userStates = []; + } + + //If the selection is "ALL" then we need to unselect everything else since this is an 'odd' filter + if (userState.key === "All") { + angular.forEach(vm.userStatesFilter, function(i) { + i.selected = false; + }); + //we can't unselect All + userState.selected = true; + //reset the selection passed to the server + vm.usersOptions.userStates = []; + } + else { + angular.forEach(vm.userStatesFilter, function (i) { + if (i.key === "All") { + i.selected = false; + } + }); + var indexOfAll = vm.usersOptions.userStates.indexOf("All"); + if (indexOfAll >= 0) { + vm.usersOptions.userStates.splice(indexOfAll, 1); + } + } + + if (userState.selected) { + vm.usersOptions.userStates.push(userState.key); + } + else { + var index = vm.usersOptions.userStates.indexOf(userState.key); + vm.usersOptions.userStates.splice(index, 1); + } + getUsers(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 581dad0c24..9e4b18034a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -111,7 +111,7 @@
diff --git a/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs b/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs index e610eec7e0..b52a653497 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs @@ -22,11 +22,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "sections")] public IEnumerable
Sections { get; set; } - [DataMember(Name = "startContentId")] - public EntityBasic StartContentId { get; set; } + [DataMember(Name = "contentStartNode")] + public EntityBasic ContentStartNode { get; set; } - [DataMember(Name = "startMediaId")] - public EntityBasic StartMediaId { get; set; } + [DataMember(Name = "mediaStartNode")] + public EntityBasic MediaStartNode { get; set; } /// /// The number of users assigned to this group diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index e2f43440aa..8056724a68 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -105,9 +105,9 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() - .ForMember(detail => detail.StartContentId, opt => opt.Ignore()) + .ForMember(detail => detail.ContentStartNode, opt => opt.Ignore()) .ForMember(detail => detail.UserCount, opt => opt.Ignore()) - .ForMember(detail => detail.StartMediaId, opt => opt.Ignore()) + .ForMember(detail => detail.MediaStartNode, opt => opt.Ignore()) .ForMember(detail => detail.Key, opt => opt.Ignore()) .ForMember(detail => detail.Sections, opt => opt.Ignore()) .ForMember(detail => detail.Notifications, opt => opt.Ignore()) @@ -122,8 +122,8 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() - .ForMember(detail => detail.StartContentId, opt => opt.Ignore()) - .ForMember(detail => detail.StartMediaId, opt => opt.Ignore()) + .ForMember(detail => detail.ContentStartNode, opt => opt.Ignore()) + .ForMember(detail => detail.MediaStartNode, opt => opt.Ignore()) .ForMember(detail => detail.Sections, opt => opt.Ignore()) .ForMember(detail => detail.Notifications, opt => opt.Ignore()) .ForMember(detail => detail.Udi, opt => opt.Ignore()) @@ -170,8 +170,8 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() - .ForMember(detail => detail.StartContentId, opt => opt.Ignore()) - .ForMember(detail => detail.StartMediaId, opt => opt.Ignore()) + .ForMember(detail => detail.ContentStartNode, opt => opt.Ignore()) + .ForMember(detail => detail.MediaStartNode, opt => opt.Ignore()) .ForMember(detail => detail.Sections, opt => opt.Ignore()) .ForMember(detail => detail.Notifications, opt => opt.Ignore()) .ForMember(detail => detail.Udi, opt => opt.Ignore()) @@ -327,12 +327,12 @@ namespace Umbraco.Web.Models.Mapping display.Sections = allSections.Where(x => Enumerable.Contains(group.AllowedSections, x.Alias)).Select(Mapper.Map); if (group.StartMediaId > 0) { - display.StartMediaId = Mapper.Map( + display.MediaStartNode = Mapper.Map( services.EntityService.Get(group.StartMediaId, UmbracoObjectTypes.Media)); } if (group.StartContentId > 0) { - display.StartContentId = Mapper.Map( + display.ContentStartNode = Mapper.Map( services.EntityService.Get(group.StartContentId, UmbracoObjectTypes.Document)); } if (display.Icon.IsNullOrWhiteSpace()) From 779306d90fa7bac6a9c8913b0bb8d6630dd4202a Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 6 Jul 2017 15:08:01 +0200 Subject: [PATCH 357/714] typos. --- .../Security/MembershipProviderExtensions.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index 645de22ab8..750526976a 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; -using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using System.Web.Security; using Umbraco.Core.Configuration; using Umbraco.Core.Models; -using Umbraco.Core.Security; using Umbraco.Core.Services; namespace Umbraco.Core.Security @@ -31,7 +26,7 @@ namespace Umbraco.Core.Security if (userService == null) return canReset; - //we need to check for the special case in which a user is an admin - in which acse they can reset the password even if EnablePasswordReset == false + //we need to check for the special case in which a user is an admin - in which case they can reset the password even if EnablePasswordReset == false if (provider.EnablePasswordReset == false) { var identity = Thread.CurrentPrincipal.GetUmbracoIdentity(); @@ -70,7 +65,7 @@ namespace Umbraco.Core.Security } /// - /// Method to get the Umbraco Members membership provider based on it's alias + /// Method to get the Umbraco Members membership provider based on its alias /// /// public static MembershipProvider GetMembersMembershipProvider() @@ -83,7 +78,7 @@ namespace Umbraco.Core.Security } /// - /// Method to get the Umbraco Users membership provider based on it's alias + /// Method to get the Umbraco Users membership provider based on its alias /// /// public static MembershipProvider GetUsersMembershipProvider() @@ -167,6 +162,5 @@ namespace Umbraco.Core.Security { return (UmbracoMembershipProviderBase)membershipProvider; } - } } From 8b308fa1ea77c5030dfa5c9b017f19e6b801d4ed Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 10 Jul 2017 12:22:12 +0200 Subject: [PATCH 358/714] hide the count until the property is set. --- .../src/views/users/views/users/users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 9e4b18034a..0293833f76 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -96,7 +96,7 @@
- Users ({{vm.usersOptions.totalItems}}) + Users ({{vm.usersOptions.totalItems}})
From f2b52585e156b397a95b38f8f2e039a3ba260808 Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 10 Jul 2017 12:22:35 +0200 Subject: [PATCH 359/714] U4-10111 Changing email on a user doesn't show the username field --- src/Umbraco.Web/Editors/UsersController.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 558b8ec265..35430e29e7 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -435,6 +435,27 @@ namespace Umbraco.Web.Editors ModelState.AddModelError("Username", "A user with the username already exists"); hasErrors = true; } + // going forward we prefer to align usernames with email, so we should cross-check to make sure + // the email or username isn't somehow being used by anyone. + existing = Services.UserService.GetByEmail(userSave.Username); + if (existing != null && existing.Id != userSave.Id) + { + ModelState.AddModelError("Username", "A user using this as their email already exists"); + hasErrors = true; + } + existing = Services.UserService.GetByUsername(userSave.Email); + if (existing != null && existing.Id != userSave.Id) + { + ModelState.AddModelError("Email", "A user using this as their username already exists"); + hasErrors = true; + } + + // if the found user has his email for username, we want to keep this synced when changing the email. + // we have already cross-checked above that the email isn't colliding with anything, so we can safely assign it here. + if (found.Username == found.Email && userSave.Username != userSave.Email) + { + userSave.Username = userSave.Email; + } var resetPasswordValue = string.Empty; if (userSave.ChangePassword != null) From a3bb11e4b03b806b50d1f5b411796c4a773f0acf Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 10 Jul 2017 13:05:33 +0200 Subject: [PATCH 360/714] I got a feeling you did this on purpose :P --- .../Persistence/Repositories/ContentRepository.cs | 4 ++-- src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs | 4 ++-- src/Umbraco.Core/Persistence/Repositories/UserRepository.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 6b99439093..cf6a9fa3fe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -947,9 +947,9 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", var filterSql = new Sql().Append("AND (cmsDocument.newest = 1)"); if (filter != null) { - foreach (var filterClaus in filter.GetWhereClauses()) + foreach (var filterClause in filter.GetWhereClauses()) { - filterSql.Append(string.Format("AND ({0})", filterClaus.Item1), filterClaus.Item2); + filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 00d77c8c13..9f35f8c592 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -592,9 +592,9 @@ namespace Umbraco.Core.Persistence.Repositories var filterSql = new Sql(); if (filter != null) { - foreach (var filterClaus in filter.GetWhereClauses()) + foreach (var filterClause in filter.GetWhereClauses()) { - filterSql.Append(string.Format("AND ({0})", filterClaus.Item1), filterClaus.Item2); + filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index a9cc613cc0..cb9c471103 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -518,9 +518,9 @@ ORDER BY colName"; if (filter != null) { - foreach (var filterClaus in filter.GetWhereClauses()) + foreach (var filterClause in filter.GetWhereClauses()) { - filterSql.Append(string.Format("AND ({0})", filterClaus.Item1), filterClaus.Item2); + filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); } } if (userGroups != null && userGroups.Length > 0) From deaece7cfc55e5636b7929c95abf85f3e4a8df09 Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 10 Jul 2017 13:37:57 +0200 Subject: [PATCH 361/714] more clauses. --- .../Persistence/Repositories/EntityRepository.cs | 8 ++++---- .../Persistence/Repositories/MediaRepository.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 4e6bf6ee70..6d01c1401e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -56,9 +56,9 @@ namespace Umbraco.Core.Persistence.Repositories { if (filter != null) { - foreach (var filterClaus in filter.GetWhereClauses()) + foreach (var filterClause in filter.GetWhereClauses()) { - sql.Where(filterClaus.Item1, filterClaus.Item2); + sql.Where(filterClause.Item1, filterClause.Item2); } } }, objectTypeId); @@ -149,9 +149,9 @@ namespace Umbraco.Core.Persistence.Repositories { if (filter != null) { - foreach (var filterClaus in filter.GetWhereClauses()) + foreach (var filterClause in filter.GetWhereClauses()) { - sql.Where(filterClaus.Item1, filterClaus.Item2); + sql.Where(filterClause.Item1, filterClause.Item2); } } }, objectTypeId); diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 9b33a62c2d..ee413c2a59 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -540,9 +540,9 @@ namespace Umbraco.Core.Persistence.Repositories var filterSql = new Sql(); if (filter != null) { - foreach (var filterClaus in filter.GetWhereClauses()) + foreach (var filterClause in filter.GetWhereClauses()) { - filterSql.Append(string.Format("AND ({0})", filterClaus.Item1), filterClaus.Item2); + filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); } } From c0a6c58a0744008cc118329eae2614962be91363 Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 10 Jul 2017 13:38:39 +0200 Subject: [PATCH 362/714] U4-10134 Disabled doesn't override Invited --- src/Umbraco.Core/Models/Membership/User.cs | 30 ++++++++++------------ src/Umbraco.Web/Editors/UsersController.cs | 1 + 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 6a814e631c..26e4f79f69 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Models.Membership [Serializable] [DataContract(IsReference = true)] public class User : Entity, IUser - { + { /// /// Constructor for creating a new/empty user /// @@ -32,7 +32,7 @@ namespace Umbraco.Core.Models.Membership //cannot be null _rawPasswordValue = ""; } - + /// /// Constructor for creating a new/empty user /// @@ -73,7 +73,7 @@ namespace Umbraco.Core.Models.Membership /// public User(int id, string name, string email, string username, string rawPasswordValue, IEnumerable userGroups, int[] startContentIds, int[] startMediaIds) : this() - { + { //we allow whitespace for this value so just check null if (rawPasswordValue == null) throw new ArgumentNullException("rawPasswordValue"); if (userGroups == null) throw new ArgumentNullException("userGroups"); @@ -255,19 +255,15 @@ namespace Umbraco.Core.Models.Membership public UserState UserState { get - { + { if (LastLoginDate == default(DateTime) && IsApproved == false && InvitedDate != null) - return UserState.Invited; - - //This state shouldn't really exist or occur - if (LastLoginDate == default(DateTime) && InvitedDate == null && RawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix)) - return UserState.NoCredentials; - - if (IsLockedOut) + return UserState.Invited; + + if (IsLockedOut) return UserState.LockedOut; - if (IsApproved == false) - return UserState.Disabled; - + if (IsApproved == false) + return UserState.Disabled; + return UserState.Active; } } @@ -357,7 +353,7 @@ namespace Umbraco.Core.Models.Membership /// /// The start media id. /// - [DataMember] + [DataMember] [DoNotClone] public int[] StartMediaIds { @@ -416,7 +412,7 @@ namespace Umbraco.Core.Models.Membership public void AddGroup(IReadOnlyUserGroup group) { if (_userGroups.Add(group)) - { + { //reset this flag so it's rebuilt with the assigned groups _allowedSections = null; OnPropertyChanged(Ps.Value.UserGroupsSelector); @@ -427,7 +423,7 @@ namespace Umbraco.Core.Models.Membership public override object DeepClone() { - var clone = (User)base.DeepClone(); + var clone = (User)base.DeepClone(); //manually clone the start node props clone._startContentIds = _startContentIds.ToArray(); clone._startMediaIds = _startMediaIds.ToArray(); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 35430e29e7..2625568c3e 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -509,6 +509,7 @@ namespace Umbraco.Web.Editors foreach (var u in users) { u.IsApproved = false; + u.InvitedDate = null; } Services.UserService.Save(users); From 79df75c415a9f86651b9f5f7a77897287f1cbce1 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 10 Jul 2017 13:39:40 +0200 Subject: [PATCH 363/714] U4-10121 - Scheduled Publishing as background task --- .../Scheduling/ScheduledPublishing.cs | 93 ++++++------------- src/Umbraco.Web/Umbraco.Web.csproj | 1 - .../WebServices/ScheduledPublishController.cs | 73 --------------- 3 files changed, 30 insertions(+), 137 deletions(-) delete mode 100644 src/Umbraco.Web/WebServices/ScheduledPublishController.cs diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 24359d2b50..b119c9a284 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -1,12 +1,18 @@ using System; -using System.Net.Http; +using System.IO; using System.Threading; using System.Threading.Tasks; +using System.Web; +using System.Web.Hosting; +using umbraco; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.Publishing; using Umbraco.Core.Sync; -using Umbraco.Web.Mvc; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; namespace Umbraco.Web.Scheduling { @@ -23,6 +29,8 @@ namespace Umbraco.Web.Scheduling _settings = settings; } + private ILogger Logger { get { return _appContext.ProfilingLogger.Logger; } } + public override async Task PerformRunAsync(CancellationToken token) { if (_appContext == null) return true; // repeat... @@ -30,10 +38,10 @@ namespace Umbraco.Web.Scheduling switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: - LogHelper.Debug("Does not run on slave servers."); + Logger.Debug("Does not run on slave servers."); return true; // DO repeat, server role can change case ServerRole.Unknown: - LogHelper.Debug("Does not run on servers with unknown role."); + Logger.Debug("Does not run on servers with unknown role."); return true; // DO repeat, server role can change } @@ -44,72 +52,31 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } - string umbracoAppUrl; try { - umbracoAppUrl = _appContext == null || _appContext.UmbracoApplicationUrl.IsNullOrWhiteSpace() - ? null - : _appContext.UmbracoApplicationUrl; - if (umbracoAppUrl.IsNullOrWhiteSpace()) + // DO not run publishing if content is re-loading + if (content.Instance.isInitializing == false) { - LogHelper.Warn("No url for service (yet), skip."); - return true; // repeat + //TODO: We should remove this in v8, this is a backwards compat hack + // because notifications will not be sent if there is no UmbracoContext + // see NotificationServiceExtensions + var httpContext = new HttpContextWrapper(new HttpContext(new SimpleWorkerRequest("temp.aspx", "", new StringWriter()))); + UmbracoContext.EnsureContext( + httpContext, + _appContext, + new WebSecurity(httpContext, _appContext), + _settings, + UrlProviderResolver.Current.Providers, + true); + + var publisher = new ScheduledPublisher(_appContext.Services.ContentService); + var count = publisher.CheckPendingAndProcess(); + Logger.Debug(() => string.Format("Processed {0} items", count)); } } catch (Exception e) { - LogHelper.Error("Could not acquire application url", e); - return true; // repeat - } - - var url = umbracoAppUrl + "/RestServices/ScheduledPublish/Index"; - - using (DisposableTimer.DebugDuration( - () => string.Format("Scheduled publishing executing @ {0}", url), - () => "Scheduled publishing complete")) - { - try - { - using (var wc = new HttpClient()) - { - var request = new HttpRequestMessage(HttpMethod.Post, url) - { - Content = new StringContent(string.Empty) - }; - - // running on a background task, requires its own (safe) scope - // (GetAuthenticationHeaderValue uses UserService to load the current user, hence requires a database) - // (might not need a scope but we don't know really) - using (var scope = ApplicationContext.Current.ScopeProvider.CreateScope()) - { - //pass custom the authorization header - request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - scope.Complete(); - } - - var result = await wc.SendAsync(request, token); - var content = await result.Content.ReadAsStringAsync(); - - if (result.IsSuccessStatusCode) - { - LogHelper.Debug( - () => string.Format( - "Request successfully sent to url = \"{0}\". ", url)); - } - else - { - var msg = string.Format( - "Request failed with status code \"{0}\". Request content = \"{1}\".", - result.StatusCode, content); - var ex = new HttpRequestException(msg); - LogHelper.Error(msg, ex); - } - } - } - catch (Exception e) - { - LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); - } + Logger.Error("Failed (see exception).", e); } return true; // repeat diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 412c7446be..db9f259c9f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1985,7 +1985,6 @@ - diff --git a/src/Umbraco.Web/WebServices/ScheduledPublishController.cs b/src/Umbraco.Web/WebServices/ScheduledPublishController.cs deleted file mode 100644 index 433930dda9..0000000000 --- a/src/Umbraco.Web/WebServices/ScheduledPublishController.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Web.Mvc; -using umbraco; -using Umbraco.Core.Logging; -using Umbraco.Core.Publishing; -using Umbraco.Web.Mvc; - -namespace Umbraco.Web.WebServices -{ - /// - /// A REST controller used for running the scheduled publishing, this is called from the background worker timer - /// - [AdminTokenAuthorize] - public class ScheduledPublishController : UmbracoController - { - private static bool _isPublishingRunning = false; - - [HttpPost] - public JsonResult Index() - { - if (_isPublishingRunning) - { - Logger.Debug(() => "Scheduled publishing is currently executing this request will exit"); - return null; - } - - _isPublishingRunning = true; - - try - { - // DO not run publishing if content is re-loading - if (content.Instance.isInitializing == false) - { - var publisher = new ScheduledPublisher(Services.ContentService); - var count = publisher.CheckPendingAndProcess(); - Logger.Debug(() => string.Format("The scheduler processed {0} items", count)); - } - - return Json(new - { - success = true - }); - - } - catch (Exception ee) - { - var errorMessage = "Error executing scheduled task"; - if (HttpContext != null && HttpContext.Request != null) - { - if (HttpContext.Request.Url != null) - errorMessage = string.Format("{0} | Request to {1}", errorMessage, HttpContext.Request.Url); - if (HttpContext.Request.UserHostAddress != null) - errorMessage = string.Format("{0} | Coming from {1}", errorMessage, HttpContext.Request.UserHostAddress); - if (HttpContext.Request.UrlReferrer != null) - errorMessage = string.Format("{0} | Referrer {1}", errorMessage, HttpContext.Request.UrlReferrer); - } - LogHelper.Error(errorMessage, ee); - - Response.StatusCode = 400; - - return Json(new - { - success = false, - message = ee.Message - }); - } - finally - { - _isPublishingRunning = false; - } - } - } -} \ No newline at end of file From ad886efa62787ef237b7db5039e5f98daa8de96d Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 10 Jul 2017 15:20:17 +0200 Subject: [PATCH 364/714] cleanup and lineendings. --- src/Umbraco.Core/Models/Membership/IUser.cs | 10 +++--- .../Repositories/ContentRepository.cs | 4 +-- src/Umbraco.Core/Services/UserService.cs | 4 +-- .../Editors/BackOfficeController.cs | 36 ++++++++----------- .../Models/ContentEditing/UserBasic.cs | 8 ++--- 5 files changed, 25 insertions(+), 37 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 70cb135f50..555537d851 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Models.Membership /// Will be left internal until a proper Membership implementation is part of the roadmap public interface IUser : IMembershipUser, IRememberBeingDirty, ICanBeDirty { - UserState UserState { get; } + UserState UserState { get; } string Name { get; set; } int SessionTimeout { get; set; } @@ -18,8 +18,8 @@ namespace Umbraco.Core.Models.Membership int[] StartMediaIds { get; set; } string Language { get; set; } - DateTime? EmailConfirmedDate { get; set; } - DateTime? InvitedDate { get; set; } + DateTime? EmailConfirmedDate { get; set; } + DateTime? InvitedDate { get; set; } /// /// Gets the groups that user is part of @@ -45,8 +45,8 @@ namespace Umbraco.Core.Models.Membership /// /// Will hold the media file system relative path of the users custom avatar if they uploaded one /// - string Avatar { get; set; } - + string Avatar { get; set; } + /// /// Returns all start node Ids assigned to the user based on both the explicit start node ids assigned to the user and any start node Ids assigned to it's user groups /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index cf6a9fa3fe..499f958449 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -661,7 +661,7 @@ namespace Umbraco.Core.Persistence.Repositories if (contentVerDto != null) { contentVersionDto.Id = contentVerDto.Id; - Database.Update(contentVersionDto); + Database.Update(contentVersionDto); } Database.Update(dto); @@ -884,7 +884,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); repo.AssignEntityPermission(entity, permission, groupIds); } - + /// /// Returns permissions directly assigned to the content item for all user groups /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index d8a3d4fb06..f7cf263e91 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -362,10 +362,8 @@ namespace Umbraco.Core.Services if (raiseEvents) uow.Events.Dispatch(SavedUser, this, new SaveEventArgs(asArray, false)); } - - } - + /// /// This is just the default user group that the membership provider will use /// diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 7821f69897..54b2f4cf1b 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -3,9 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Net; -using System.Net.Configuration; -using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -23,7 +20,6 @@ using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; -using Umbraco.Core.Models; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; @@ -32,9 +28,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.Security.Identity; using Umbraco.Web.Trees; using Umbraco.Web.UI.JavaScript; -using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Services; -using Umbraco.Web.Security; using Action = umbraco.BusinessLogic.Actions.Action; using Constants = Umbraco.Core.Constants; @@ -78,7 +72,7 @@ namespace Umbraco.Web.Editors () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"), () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml")); } - + [HttpGet] public async Task VerifyInvite(string invite) { @@ -86,17 +80,17 @@ namespace Umbraco.Web.Editors { Logger.Warn("VerifyUser endpoint reached with invalid token: NULL"); return RedirectToAction("Default"); - } + } var parts = Server.UrlDecode(invite).Split('|'); if (parts.Length != 2) - { + { Logger.Warn("VerifyUser endpoint reached with invalid token: " + invite); return RedirectToAction("Default"); } - var token = parts[1]; + var token = parts[1]; var decoded = token.FromUrlBase64(); if (decoded.IsNullOrWhiteSpace()) @@ -111,7 +105,7 @@ namespace Umbraco.Web.Editors { Logger.Warn("VerifyUser endpoint reached with invalid token: " + invite); return RedirectToAction("Default"); - } + } var identityUser = await UserManager.FindByIdAsync(intId); if (identityUser == null) @@ -127,8 +121,8 @@ namespace Umbraco.Web.Editors Logger.Warn("Could not verify email, Error: " + string.Join(",", result.Errors) + ", Token: " + invite); return RedirectToAction("Default"); } - - //sign the user in + + //sign the user in AuthenticationManager.SignOut( Core.Constants.Security.BackOfficeAuthenticationType, @@ -137,7 +131,7 @@ namespace Umbraco.Web.Editors await SignInManager.SignInAsync(identityUser, false, false); return new RedirectResult(Url.Action("Default") + "#/login/false?invite=1"); - } + } /// /// This Action is used by the installer when an upgrade is detected but the admin user is not logged in. We need to @@ -255,7 +249,7 @@ namespace Umbraco.Web.Editors [MinifyJavaScriptResult(Order = 1)] public JavaScriptResult ServerVariables() { - var serverVars = new BackOfficeServerVariables(Url, ApplicationContext, UmbracoConfig.For.UmbracoSettings()); + var serverVars = new BackOfficeServerVariables(Url, ApplicationContext, UmbracoConfig.For.UmbracoSettings()); //cache the result if debugging is disabled var result = HttpContext.IsDebuggingEnabled @@ -267,8 +261,8 @@ namespace Umbraco.Web.Editors return JavaScript(result); } - - + + [HttpPost] public ActionResult ExternalLogin(string provider, string redirectUrl = null) @@ -447,7 +441,7 @@ namespace Umbraco.Web.Editors if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); - var groups = Services.UserService.GetUserGroupsByAlias(autoLinkOptions.GetDefaultUserGroups(UmbracoContext, loginInfo)); + var groups = Services.UserService.GetUserGroupsByAlias(autoLinkOptions.GetDefaultUserGroups(UmbracoContext, loginInfo)); var autoLinkUser = new BackOfficeIdentityUser { @@ -455,9 +449,9 @@ namespace Umbraco.Web.Editors Name = loginInfo.ExternalIdentity.Name, AllowedSections = autoLinkOptions.GetDefaultAllowedSections(UmbracoContext, loginInfo), Culture = autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo), - UserName = loginInfo.Email, + UserName = loginInfo.Email, Groups = groups.Select(x => x.ToReadOnlyGroup()).ToArray() - }; + }; //call the callback if one is assigned if (autoLinkOptions.OnAutoLinking != null) @@ -501,8 +495,6 @@ namespace Umbraco.Web.Editors return false; } - - /// /// Returns the JavaScript blocks for any legacy trees declared diff --git a/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs b/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs index 7467955e71..0b4a7e02d7 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs @@ -5,9 +5,7 @@ using System.Runtime.Serialization; using Umbraco.Core.Models.Membership; namespace Umbraco.Web.Models.ContentEditing -{ - - +{ /// /// The user model used for paging and listing users in the UI /// @@ -59,8 +57,8 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "isCurrentUser")] [ReadOnly(true)] - public bool IsCurrentUser { get; set; } - + public bool IsCurrentUser { get; set; } + /// /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. /// From 1149129c6f7efe55d3f8e2401304bd56064fb3cf Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 10 Jul 2017 15:21:24 +0200 Subject: [PATCH 365/714] U4-10104 Dashboards are not showing up in backoffice --- src/Umbraco.Web/Editors/DashboardSecurity.cs | 59 ++++++++++++++++---- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web/Editors/DashboardSecurity.cs b/src/Umbraco.Web/Editors/DashboardSecurity.cs index 879fd468e0..24a492ca16 100644 --- a/src/Umbraco.Web/Editors/DashboardSecurity.cs +++ b/src/Umbraco.Web/Editors/DashboardSecurity.cs @@ -61,18 +61,57 @@ namespace Umbraco.Web.Editors { var allowedSoFar = false; - //Check if this item as any grant-by-section arguments, if so check if the user has access to any of the sections approved, if so they will - // be allowed to see it (so far) - if (grantedBySectionTypes.Any()) + // if there's no grantBySection or grant rules defined - we allow access so far and skip to checking deny rules + if (grantedBySectionTypes.Any() == false && grantedTypes.Any() == false) { - var allowedApps = sectionService.GetAllowedSections(Convert.ToInt32(user.Id)) - .Select(x => x.Alias) - .ToArray(); - - var allApprovedSections = grantedBySectionTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - if (allApprovedSections.Any(allowedApps.Contains)) + allowedSoFar = true; + } + // else we check the rules and only allow if one matches + else + { + // check if this item has any grant-by-section arguments, if so check if the user has access to any of the sections approved, if so they will + // be allowed to see it (so far) + if (grantedBySectionTypes.Any()) { - allowedSoFar = true; + var allowedApps = sectionService.GetAllowedSections(Convert.ToInt32(user.Id)) + .Select(x => x.Alias) + .ToArray(); + + var allApprovedSections = grantedBySectionTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + if (allApprovedSections.Any(allowedApps.Contains)) + { + allowedSoFar = true; + } + } + + // check if this item as any grant arguments, if so check if the user is in one of the user groups approved, if so they will + // be allowed to see it (so far) + if (grantedTypes.Any()) + { + var allApprovedUserTypes = grantedTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + foreach (var userGroup in user.Groups) + { + if (allApprovedUserTypes.InvariantContains(userGroup.Alias)) + { + allowedSoFar = true; + break; + } + } + } + } + + // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will + // be denied to see it no matter what + if (denyTypes.Any()) + { + var allDeniedUserTypes = denyTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + foreach (var userGroup in user.Groups) + { + if (allDeniedUserTypes.InvariantContains(userGroup.Alias)) + { + allowedSoFar = false; + break; + } } } From 6a06a9e40039de026b91a666e7bd0b18dd55d956 Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 10 Jul 2017 15:27:15 +0200 Subject: [PATCH 366/714] small tweak to skip a check if not needed. --- src/Umbraco.Web/Editors/DashboardSecurity.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Editors/DashboardSecurity.cs b/src/Umbraco.Web/Editors/DashboardSecurity.cs index 24a492ca16..d1db432d6c 100644 --- a/src/Umbraco.Web/Editors/DashboardSecurity.cs +++ b/src/Umbraco.Web/Editors/DashboardSecurity.cs @@ -69,8 +69,8 @@ namespace Umbraco.Web.Editors // else we check the rules and only allow if one matches else { - // check if this item has any grant-by-section arguments, if so check if the user has access to any of the sections approved, if so they will - // be allowed to see it (so far) + // check if this item has any grant-by-section arguments. + // if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far) if (grantedBySectionTypes.Any()) { var allowedApps = sectionService.GetAllowedSections(Convert.ToInt32(user.Id)) @@ -84,9 +84,9 @@ namespace Umbraco.Web.Editors } } - // check if this item as any grant arguments, if so check if the user is in one of the user groups approved, if so they will - // be allowed to see it (so far) - if (grantedTypes.Any()) + // if not already granted access, check if this item as any grant arguments. + // if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far) + if (allowedSoFar == false && grantedTypes.Any()) { var allApprovedUserTypes = grantedTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); foreach (var userGroup in user.Groups) From cb4e1d324d8bb681691e2879e64df885b2e6d334 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 11 Jul 2017 12:18:12 +1000 Subject: [PATCH 367/714] Removes the NoCredentials enum (we don't use it or need it) --- src/Umbraco.Core/Models/Membership/UserState.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/UserState.cs b/src/Umbraco.Core/Models/Membership/UserState.cs index cfbb5444bb..5f6ee1615a 100644 --- a/src/Umbraco.Core/Models/Membership/UserState.cs +++ b/src/Umbraco.Core/Models/Membership/UserState.cs @@ -9,14 +9,6 @@ Active = 0, Disabled = 1, LockedOut = 2, - Invited = 3, - - /// - /// Occurs when the user has been created (not invited) and has no credentials assigned - /// - /// - /// This state shouldn't really exist or occur - /// - NoCredentials = 100 + Invited = 3 } } \ No newline at end of file From 9462f8ad8580ee629c7b043014088a40eeaf8b83 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 11 Jul 2017 13:57:33 +1000 Subject: [PATCH 368/714] Fixes password reset when there is invalid other password properties --- .../validation/valcompare.directive.js | 12 +- .../components/users/change-password.html | 114 +++++++++--------- 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js index 1a36dcc24f..195c70ce0f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js @@ -1,11 +1,13 @@ angular.module('umbraco.directives.validation') .directive('valCompare',function () { return { - require: "ngModel", - link: function (scope, elem, attrs, ctrl) { - - //TODO: Pretty sure this should be done using a requires ^form in the directive declaration - var otherInput = elem.inheritedData("$formController")[attrs.valCompare]; + require: ["ngModel", "^form"], + link: function (scope, elem, attrs, ctrls) { + + var ctrl = ctrls[0]; + var formCtrl = ctrls[1]; + + var otherInput = formCtrl[attrs.valCompare]; ctrl.$parsers.push(function(value) { if(value === otherInput.$viewValue) { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index d0a42299ff..a4e99546fa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -1,62 +1,68 @@
-
- Password has been reset to: -
- {{passwordValues.generatedPassword}} -
-
-
- - Change password - +
+ Password has been reset to: +
+ {{passwordValues.generatedPassword}}
-
- - - - - +
+ +
+ + + + + - - - - Required - - + + + + Required + + - - - Required - Minimum {{$parent.config.minPasswordLength}} characters - - + + + Required + Minimum {{$parent.config.minPasswordLength}} characters + + - - + + - - The confirmed password doesn't match the new password! - - + + The confirmed password doesn't match the new password! + + - - Cancel - -
-
+ + Cancel + + + +
+
From 3c31e330001fa7b9a4d92c068d5deb272b139165 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 11 Jul 2017 14:46:51 +1000 Subject: [PATCH 369/714] Fixes U4-10133 Investigate issues with SMTP Invite and async --- src/Umbraco.Core/Security/EmailService.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index a461f62926..d609534797 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -1,25 +1,25 @@ using System.Net.Mail; using System.Threading.Tasks; using Microsoft.AspNet.Identity; +using Umbraco.Core.Configuration; namespace Umbraco.Core.Security { public class EmailService : IIdentityMessageService { - public Task SendAsync(IdentityMessage message) + public async Task SendAsync(IdentityMessage message) { using (var client = new SmtpClient()) - using (var mailMessage = new MailMessage()) - { - mailMessage.Body = message.Body; - mailMessage.To.Add(message.Destination); - mailMessage.Subject = message.Subject; - - //TODO: This check could be nicer but that is the way it is currently + using (var mailMessage = new MailMessage( + UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, + message.Destination, + message.Subject, + message.Body)) + { mailMessage.IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false && message.Body.Contains("<") && message.Body.Contains(" Date: Tue, 11 Jul 2017 15:06:18 +1000 Subject: [PATCH 370/714] Doesn't send async if not using a network based SMTP server --- src/Umbraco.Core/Security/EmailService.cs | 32 +++++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index d609534797..f8f9af10ae 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -9,17 +9,33 @@ namespace Umbraco.Core.Security { public async Task SendAsync(IdentityMessage message) { - using (var client = new SmtpClient()) - using (var mailMessage = new MailMessage( - UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, + var mailMessage = new MailMessage( + UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, message.Destination, message.Subject, - message.Body)) - { - mailMessage.IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false - && message.Body.Contains("<") && message.Body.Contains(" Date: Tue, 11 Jul 2017 19:19:38 +1000 Subject: [PATCH 371/714] Gets implicit/inherited permissions 'working', well at least with one test, now need to test the rest. --- .../Models/Membership/EntityPermission.cs | 16 + .../Models/Membership/IReadOnlyUserGroup.cs | 8 + .../Models/Membership/ReadOnlyUserGroup.cs | 12 +- .../Models/Membership/UserGroup.cs | 2 +- .../Models/Membership/UserGroupExtensions.cs | 9 +- .../Repositories/ContentRepository.cs | 34 +- .../Repositories/PermissionRepository.cs | 119 ++++--- src/Umbraco.Core/Services/IUserService.cs | 24 +- src/Umbraco.Core/Services/UserService.cs | 290 ++++++++++++------ .../Repositories/ContentRepositoryTest.cs | 20 +- .../Services/ContentServiceTests.cs | 172 +++++++++++ .../Services/UserServiceTests.cs | 117 ++++--- .../umbraco/dialogs/cruds.aspx.cs | 2 +- src/umbraco.businesslogic/User.cs | 3 +- 14 files changed, 585 insertions(+), 243 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs index 15bdb4cbe9..a1fa57d274 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs @@ -11,6 +11,14 @@ namespace Umbraco.Core.Models.Membership { EntityId = entityId; AssignedPermissions = assignedPermissions; + IsDefaultPermissions = false; + } + + public EntityPermission(int entityId, string[] assignedPermissions, bool isDefaultPermissions) + { + EntityId = entityId; + AssignedPermissions = assignedPermissions; + IsDefaultPermissions = isDefaultPermissions; } public int EntityId { get; private set; } @@ -19,6 +27,14 @@ namespace Umbraco.Core.Models.Membership /// The assigned permissions for the user/entity combo ///
public string[] AssignedPermissions { get; private set; } + + /// + /// True if the permissions assigned to this object are the group's default permissions and not explicitly defined permissions + /// + /// + /// This will be the case when looking up entity permissions and falling back to the default permissions + /// + public bool IsDefaultPermissions { get; private set; } /// /// Adds additional permissions to an existing instance of diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index beffc5862d..deebc03401 100644 --- a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -18,6 +18,14 @@ namespace Umbraco.Core.Models.Membership /// string Alias { get; } + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. + /// + IEnumerable Permissions { get; set; } + IEnumerable AllowedSections { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs index b2a7349e81..c1b8d3997f 100644 --- a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs @@ -5,13 +5,15 @@ namespace Umbraco.Core.Models.Membership { public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable { - public ReadOnlyUserGroup(int id, string name, string icon, int? startContentId, int? startMediaId, string @alias, IEnumerable allowedSections) + public ReadOnlyUserGroup(int id, string name, string icon, int? startContentId, int? startMediaId, string @alias, + IEnumerable allowedSections, IEnumerable permissions) { Name = name; Icon = icon; Id = id; Alias = alias; AllowedSections = allowedSections; + Permissions = permissions; //Zero is invalid and will be treated as Null StartContentId = startContentId == 0 ? null : startContentId; @@ -24,6 +26,14 @@ namespace Umbraco.Core.Models.Membership public int? StartContentId { get; private set; } public int? StartMediaId { get; private set; } public string Alias { get; private set; } + + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. + /// + public IEnumerable Permissions { get; set; } public IEnumerable AllowedSections { get; private set; } public bool Equals(ReadOnlyUserGroup other) diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 29afc86fc8..012e9b6cc2 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Membership ///
[Serializable] [DataContract(IsReference = true)] - internal class UserGroup : Entity, IUserGroup + internal class UserGroup : Entity, IUserGroup, IReadOnlyUserGroup { private int? _startContentId; private int? _startMediaId; diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs index d7625f0fd8..34f4b805f9 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs @@ -7,12 +7,17 @@ namespace Umbraco.Core.Models.Membership { public static IReadOnlyUserGroup ToReadOnlyGroup(this IUserGroup group) { - return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedSections); + //this will generally always be the case + var readonlyGroup = group as IReadOnlyUserGroup; + if (readonlyGroup != null) return readonlyGroup; + + //otherwise create one + return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedSections, group.Permissions); } public static IReadOnlyUserGroup ToReadOnlyGroup(this UserGroupDto group) { - return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.UserGroup2AppDtos.Select(x => x.AppAlias).ToArray()); + return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.UserGroup2AppDtos.Select(x => x.AppAlias).ToArray(), group.DefaultPermissions.ToCharArray().Select(x => x.ToString())); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 499f958449..f07e9c8385 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -470,24 +470,24 @@ namespace Umbraco.Core.Persistence.Repositories entity.SortOrder = sortOrder; entity.Level = level; - //Assign the same permissions to it as the parent node - // http://issues.umbraco.org/issue/U4-2161 - var permissionsRepo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - var parentPermissions = permissionsRepo.GetPermissionsForEntity(entity.ParentId).ToArray(); - //if there are parent permissions then assign them, otherwise leave null and permissions will become the - // user's default permissions. - if (parentPermissions.Any()) - { - var userGroupPermissions = ( - from perm in parentPermissions - from p in perm.AssignedPermissions - select new EntityPermissionSet.UserGroupPermission(perm.UserGroupId, p)).ToList(); + ////Assign the same permissions to it as the parent node + //// http://issues.umbraco.org/issue/U4-2161 + //var permissionsRepo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + //var parentPermissions = permissionsRepo.GetPermissionsForEntity(entity.ParentId).ToArray(); + ////if there are parent permissions then assign them, otherwise leave null and permissions will become the + //// user's default permissions. + //if (parentPermissions.Length > 0) + //{ + // var userGroupPermissions = ( + // from perm in parentPermissions + // from p in perm.AssignedPermissions + // select new EntityPermissionSet.UserGroupPermission(perm.UserGroupId, p)).ToList(); - permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userGroupPermissions)); - //flag the entity's permissions changed flag so we can track those changes. - //Currently only used for the cache refreshers to detect if we should refresh all user permissions cache. - ((Content)entity).PermissionsChanged = true; - } + // permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userGroupPermissions)); + // //flag the entity's permissions changed flag so we can track those changes. + // //Currently only used for the cache refreshers to detect if we should refresh all user permissions cache. + // ((Content)entity).PermissionsChanged = true; + //} //Create the Content specific data - cmsContent var contentDto = dto.ContentVersionDto.ContentDto; diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index ca9045507b..5ae4efad45 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -35,82 +35,68 @@ namespace Umbraco.Core.Persistence.Repositories } /// - /// Returns permissions for a given group for any number of nodes + /// Returns explicitly defined permissions for a user group for any number of nodes /// /// /// /// public IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds) { - var entityIdKey = GetEntityIdKey(entityIds); - return _runtimeCache.GetCacheItem>( - string.Format("{0}{1}{2}", - CacheKeys.UserGroupPermissionsCacheKey, - groupId, - entityIdKey), - () => - { - var whereCriteria = GetPermissionsForEntitiesCriteria(groupId, entityIds); - var sql = new Sql(); - sql.Select("*") - .From() - .Where(whereCriteria); - var result = _unitOfWork.Database.Fetch(sql).ToArray(); - // ToArray() to ensure it's all fetched from the db once - return ConvertToPermissionList(result); - }, - GetCacheTimeout(), - priority: GetCachePriority()); - } - - private static string GetEntityIdKey(int[] entityIds) - { - return string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); - } - - private string GetPermissionsForEntitiesCriteria(int groupId, params int[] entityIds) - { - var whereBuilder = new StringBuilder(); - whereBuilder.Append(_sqlSyntax.GetQuotedColumnName("userGroupId")); - whereBuilder.Append("="); - whereBuilder.Append(groupId); - - if (entityIds.Any()) + //var whereCriteria = GetPermissionsForEntitiesCriteria(groupId, entityIds); + var result = new List(); + //iterate in groups of 2000 since we don't want to exceed the max SQL param count + foreach (var groupOfIds in entityIds.InGroupsOf(2000)) { - whereBuilder.Append(" AND "); + var ids = groupOfIds; - //where nodeId = @nodeId1 OR nodeId = @nodeId2, etc... - whereBuilder.Append("("); - for (var index = 0; index < entityIds.Length; index++) - { - var entityId = entityIds[index]; - whereBuilder.Append(_sqlSyntax.GetQuotedColumnName("nodeId")); - whereBuilder.Append("="); - whereBuilder.Append(entityId); - if (index < entityIds.Length - 1) - { - whereBuilder.Append(" OR "); - } - } - - whereBuilder.Append(")"); - } - - return whereBuilder.ToString(); + var sql = new Sql(); + sql.Select("*") + .From(_sqlSyntax) + .Where(dto => dto.UserGroupId == groupId && ids.Contains(dto.NodeId), _sqlSyntax); + var permissions = _unitOfWork.Database.Fetch(sql); + result.AddRange(ConvertToPermissionList(permissions)); + } + return result; + + + //var entityIdKey = GetEntityIdKey(entityIds); + + //return _runtimeCache.GetCacheItem>( + // string.Format("{0}{1}{2}", + // CacheKeys.UserGroupPermissionsCacheKey, + // groupId, + // entityIdKey), + // () => + // { + // var whereCriteria = GetPermissionsForEntitiesCriteria(groupId, entityIds); + // var sql = new Sql(); + // sql.Select("*") + // .From() + // .Where(whereCriteria); + // var result = _unitOfWork.Database.Fetch(sql); + // return ConvertToPermissionList(result); + // }, + // GetCacheTimeout(), + // priority: GetCachePriority()); } - private static TimeSpan GetCacheTimeout() - { - //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will only have this exist in cache for 20 minutes, - // then it will refresh from the database. - return new TimeSpan(0, 20, 0); - } + //private static string GetEntityIdKey(int[] entityIds) + //{ + // return string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); + //} - private static CacheItemPriority GetCachePriority() - { - //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will make this priority below average - return CacheItemPriority.BelowNormal; - } + //private static TimeSpan GetCacheTimeout() + //{ + // //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will only have this exist in cache for 20 minutes, + // // then it will refresh from the database. + // return new TimeSpan(0, 20, 0); + //} + + //private static CacheItemPriority GetCachePriority() + //{ + // //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will make this priority below average + // return CacheItemPriority.BelowNormal; + //} private static IEnumerable ConvertToPermissionList(IEnumerable result) { @@ -142,8 +128,7 @@ namespace Umbraco.Core.Persistence.Repositories .Where(dto => dto.NodeId == entityId) .OrderBy(dto => dto.NodeId); - var result = _unitOfWork.Database.Fetch(sql).ToArray(); - // ToArray() to ensure it's all fetched from the db once + var result = _unitOfWork.Database.Fetch(sql); return ConvertToPermissionList(result); } diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 3ac485bf2b..b55048ef4d 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -92,63 +92,63 @@ namespace Umbraco.Core.Services /// User to retrieve permissions for /// Specifiying nothing will return all user permissions for all nodes /// An enumerable list of + /// + /// > This is the ONLY one for permissions from 7.6! + /// IEnumerable GetPermissions(IUser user, params int[] nodeIds); /// /// Get permissions set for a group and optional node Ids /// /// Group to retrieve permissions for - /// + /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - IEnumerable GetPermissions(string groupAlias, bool directlyAssignedOnly, params int[] nodeIds); + IEnumerable GetPermissions(string groupAlias, bool fallbackToDefaultPermissions, params int[] nodeIds); /// /// Get permissions set for a group and optional node Ids /// /// Group to retrieve permissions for - /// + /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - IEnumerable GetPermissions(IUserGroup group, bool directlyAssignedOnly, params int[] nodeIds); + IEnumerable GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); /// /// Gets the permissions for the provided user and path /// /// User to check permissions for /// Path to check permissions for - /// String indicating permissions for provided user and path - string GetPermissionsForPath(IUser user, string path); + EntityPermissionSet GetPermissionsForPath(IUser user, string path); /// /// Gets the permissions for the provided group and path /// /// Group alias to check permissions for /// Path to check permissions for - /// + /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// - /// String indicating permissions for provided user and path - string GetPermissionsForPath(string groupAlias, string path, bool directlyAssignedOnly = true); + EntityPermission GetPermissionsForPath(string groupAlias, string path, bool fallbackToDefaultPermissions = false); /// /// Gets the permissions for the provided group and path /// /// Group to check permissions for /// Path to check permissions for - /// + /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// - /// String indicating permissions for provided user and path - string GetPermissionsForPath(IUserGroup group, string path, bool directlyAssignedOnly = true); + EntityPermission GetPermissionsForPath(IUserGroup group, string path, bool fallbackToDefaultPermissions = false); /// /// Replaces the same permission set for a single group to any number of entities diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index f7cf263e91..b2a1ddec58 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -121,7 +121,7 @@ namespace Umbraco.Core.Services Language = GlobalSettings.DefaultUILanguage, Name = username, RawPasswordValue = passwordValue, - Username = username, + Username = username, IsLockedOut = false, IsApproved = isApproved }; @@ -208,11 +208,11 @@ namespace Umbraco.Core.Services public void Delete(IUser membershipUser) { //disable - membershipUser.IsApproved = false; - + membershipUser.IsApproved = false; + Save(membershipUser); - } - + } + /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// @@ -616,12 +616,12 @@ namespace Umbraco.Core.Services { return repository.GetAllNotInGroup(groupId); } - } - + } + #endregion - + #region Implementation of IUserService - + /// /// Gets an IProfile by User Id. /// @@ -706,8 +706,8 @@ namespace Umbraco.Core.Services repository.AssignGroupPermission(groupId, permission, entityIds); uow.Commit(); } - } - + } + /// /// Gets all UserGroups or those specified as parameters /// @@ -720,8 +720,8 @@ namespace Umbraco.Core.Services var repository = RepositoryFactory.CreateUserGroupRepository(uow); return repository.GetAll(ids).OrderBy(x => x.Name); } - } - + } + public IEnumerable GetUserGroupsByAlias(params string[] aliases) { if (aliases.Length == 0) return Enumerable.Empty(); @@ -788,8 +788,8 @@ namespace Umbraco.Core.Services } var repository = RepositoryFactory.CreateUserGroupRepository(uow); - repository.AddOrUpdateGroupWithUsers(userGroup, userIds); - + repository.AddOrUpdateGroupWithUsers(userGroup, userIds); + uow.Commit(); if (raiseEvents) @@ -838,8 +838,8 @@ namespace Umbraco.Core.Services uow.Commit(); //TODO: Events? } - } - + } + /// /// Get permissions set for a user and node Id /// @@ -848,16 +848,19 @@ namespace Umbraco.Core.Services /// An enumerable list of public IEnumerable GetPermissions(IUser user, params int[] nodeIds) { + if (nodeIds.Length == 0) + return Enumerable.Empty(); + var result = new List(); + foreach (var group in user.Groups) { - //TODO: This may perform horribly :/ - foreach (var permission in GetPermissions(group.Alias, false, nodeIds)) + foreach (var permission in GetPermissions(group, true, nodeIds)) { AddOrAmendPermissionList(result, permission); } - } - + } + return result; } @@ -865,19 +868,57 @@ namespace Umbraco.Core.Services /// Get permissions set for a group and node Id /// /// Group to retrieve permissions for - /// + /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - public IEnumerable GetPermissions(string groupAlias, bool directlyAssignedOnly, params int[] nodeIds) - { + public IEnumerable GetPermissions(string groupAlias, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (nodeIds.Length == 0) + return Enumerable.Empty(); + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); var group = repository.Get(groupAlias); - return GetPermissionsInternal(repository, group, directlyAssignedOnly, nodeIds); + if (group == null) throw new InvalidOperationException("No group found with alias " + groupAlias); + return GetPermissionsInternal(repository, group.ToReadOnlyGroup(), fallbackToDefaultPermissions, nodeIds); + } + } + + private IEnumerable GetPermissions(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (group == null) throw new ArgumentNullException("group"); + if (nodeIds.Length == 0) + return Enumerable.Empty(); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateUserGroupRepository(uow); + return GetPermissionsInternal(repository, group, fallbackToDefaultPermissions, nodeIds); + } + } + + private IEnumerable GetPermissions(int groupId, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (nodeIds.Length == 0) + return Enumerable.Empty(); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateUserGroupRepository(uow); + + if (fallbackToDefaultPermissions == false) + { + //if fallbackToDefaultPermissions is false, we don't have to lookup the group + return repository.GetPermissionsForEntities(groupId, nodeIds); + } + + var group = repository.Get(groupId); + if (group == null) throw new InvalidOperationException("No group found with id " + groupId); + return GetPermissionsInternal(repository, group.ToReadOnlyGroup(), true, nodeIds); } } @@ -885,35 +926,40 @@ namespace Umbraco.Core.Services /// Get permissions set for a group and optional node Ids ///
/// Group to retrieve permissions for - /// + /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - public IEnumerable GetPermissions(IUserGroup group, bool directlyAssignedOnly, params int[] nodeIds) + public IEnumerable GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) { if (group == null) throw new ArgumentNullException("group"); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); - return GetPermissionsInternal(repository, group, directlyAssignedOnly, nodeIds); + return GetPermissionsInternal(repository, group.ToReadOnlyGroup(), fallbackToDefaultPermissions, nodeIds); } - } - - private IEnumerable GetPermissionsInternal(IUserGroupRepository repository, IUserGroup group, bool directlyAssignedOnly, params int[] nodeIds) + } + + private static IEnumerable GetPermissionsInternal(IUserGroupRepository repository, IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) { + if (group == null) throw new ArgumentNullException("group"); + + if (nodeIds.Length == 0) + return Enumerable.Empty(); + var explicitPermissions = repository.GetPermissionsForEntities(group.Id, nodeIds); var result = new List(explicitPermissions); // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults - if (directlyAssignedOnly == false) + if (fallbackToDefaultPermissions) { - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToList(); - if (missingIds.Any()) + var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToArray(); + if (missingIds.Length > 0) { result.AddRange(missingIds - .Select(i => new EntityPermission(i, group.Permissions.ToArray()))); + .Select(i => new EntityPermission(i, group.Permissions.ToArray(), isDefaultPermissions: true))); } } return result; @@ -946,91 +992,163 @@ namespace Umbraco.Core.Services ///
/// User to check permissions for /// Path to check permissions for - /// String indicating permissions for provided user and path - public string GetPermissionsForPath(IUser user, string path) - { - var assignedPermissions = GetPermissionsForGroupsAndPath(user.Groups.Select(x => x.Alias), path); - return GetAggregatePermissions(assignedPermissions); + public EntityPermissionSet GetPermissionsForPath(IUser user, string path) + { + var nodeIds = GetIdsFromPath(path); + + if (nodeIds.Length == 0) + return null; + + var permissionsByGroup = GetPermissionsForGroupsAndPath(user.Groups, nodeIds); + + // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? + if (permissionsByGroup.Count == 0) + return null; + + var entityId = nodeIds[0]; + + var groupPermissions = new List(); + foreach (var entityPermission in permissionsByGroup) + { + var groupId = entityPermission.Key; + foreach (var assignedPermission in entityPermission.Value.AssignedPermissions) + { + groupPermissions.Add(new EntityPermissionSet.UserGroupPermission(groupId, assignedPermission)); + } + } + + var permissionSet = new EntityPermissionSet(entityId, groupPermissions); + return permissionSet; } /// /// Retrieves the permissions assigned to each group for a given path /// /// List of groups associated with the user - /// Path to check permissions for - /// List of strings indicating permissions for each groups - private IEnumerable GetPermissionsForGroupsAndPath(IEnumerable groups, string path) + /// Path to check permissions for + /// A dictionary of group ids and their associated node permissions + private IDictionary GetPermissionsForGroupsAndPath(IEnumerable groups, int[] pathIds) { return groups - .Select(g => GetPermissionsForPath(g, path, directlyAssignedOnly: false)) - .ToList(); - } - - /// - /// Aggregates a set of permissions strings to return a unique permissions string containing the most permissive set - /// - /// List of permission strings - /// Single permission string - private static string GetAggregatePermissions(IEnumerable assignedPermissions) - { - return string.Join(string.Empty, assignedPermissions - .SelectMany(s => s.ToCharArray()) - .Distinct()); - } - + .Select(g => new + { + group = g.Id, + permissions = GetPermissionsForPath(g, pathIds, fallbackToDefaultPermissions: true) + }) + .ToDictionary(x => x.group, x => x.permissions); + } + /// /// Gets the permissions for the provided group and path /// /// User to check permissions for /// Path to check permissions for - /// + /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// /// String indicating permissions for provided user and path - public string GetPermissionsForPath(string groupAlias, string path, bool directlyAssignedOnly = true) + public EntityPermission GetPermissionsForPath(string groupAlias, string path, bool fallbackToDefaultPermissions = false) { - var nodeId = GetNodeIdFromPath(path); - var permission = GetPermissions(groupAlias, directlyAssignedOnly, nodeId) - .FirstOrDefault(); - return permission != null - ? string.Join(string.Empty, permission.AssignedPermissions) - : string.Empty; - } - + var nodeIds = GetIdsFromPath(path); + //get permissions for all nodes in the path + var permissions = GetPermissions(groupAlias, fallbackToDefaultPermissions, nodeIds); + return GetPermissionsForPath(permissions, nodeIds, fallbackToDefaultPermissions); + } + /// /// Gets the permissions for the provided group and path /// /// Group to check permissions for /// Path to check permissions for - /// + /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// /// String indicating permissions for provided user and path - public string GetPermissionsForPath(IUserGroup group, string path, bool directlyAssignedOnly = true) + public EntityPermission GetPermissionsForPath(IUserGroup group, string path, bool fallbackToDefaultPermissions = false) { - var nodeId = GetNodeIdFromPath(path); - var permission = GetPermissions(group, directlyAssignedOnly, nodeId) - .FirstOrDefault(); - return permission != null - ? string.Join(string.Empty, permission.AssignedPermissions) - : string.Empty; + var nodeIds = GetIdsFromPath(path); + return GetPermissionsForPath(group.ToReadOnlyGroup(), nodeIds, fallbackToDefaultPermissions); } + private EntityPermission GetPermissionsForPath(IReadOnlyUserGroup group, int[] pathIds, bool fallbackToDefaultPermissions = false) + { + //get permissions for all nodes in the path + var permissions = GetPermissions(group, fallbackToDefaultPermissions, pathIds); + return GetPermissionsForPath(permissions, pathIds, fallbackToDefaultPermissions); + } + + //private EntityPermission GetPermissionsForPath(int groupId, string path, bool fallbackToDefaultPermissions = false) + //{ + // var nodeIds = GetIdsFromPath(path); + // //get permissions for all nodes in the path + // var permissions = GetPermissions(groupId, fallbackToDefaultPermissions, nodeIds); + // return GetPermissionsForPath(permissions, groupId, nodeIds, fallbackToDefaultPermissions); + //} + + /// + /// Returns the permissions for the path ids + /// + /// + /// Must be ordered deepest to shallowest (right to left) + /// + /// + private static EntityPermission GetPermissionsForPath( + IEnumerable pathPermissions, + int[] pathIds, + bool fallbackToDefaultPermissions = false) + { + //get permissions for all nodes in the path + var permissionsByEntityId = pathPermissions.ToDictionary(x => x.EntityId, x => x); + + //then the permissions assigned to the path will be the 'deepest' node found that has permissions + foreach (var id in pathIds) + { + EntityPermission permission; + if (permissionsByEntityId.TryGetValue(id, out permission)) + { + //don't return the default permissions if that is the one assigned here (we'll do that below if nothing was found) + if (permission.IsDefaultPermissions == false) + return permission; + } + } + + //if we've made it here it means that no implicit/inherited permissions were found so we return the defaults if that is specified + if (fallbackToDefaultPermissions == false) + return null; + + return permissionsByEntityId[pathIds[0]]; + } /// - /// Parses a path to find the lowermost node id + /// Convert a path to node ids in the order from right to left (deepest to shallowest) /// - /// Path as string - /// Node id - private static int GetNodeIdFromPath(string path) + /// + /// + private int[] GetIdsFromPath(string path) { - return path.Contains(",") - ? int.Parse(path.Substring(path.LastIndexOf(",", StringComparison.Ordinal) + 1)) - : int.Parse(path); - } - + var nodeIds = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.TryConvertTo()) + .Where(x => x.Success) + .Select(x => x.Result) + .Reverse() + .ToArray(); + return nodeIds; + } + + ///// + ///// Parses a path to find the lowermost node id + ///// + ///// Path as string + ///// Node id + //private static int GetNodeIdFromPath(string path) + //{ + // return path.Contains(",") + // ? int.Parse(path.Substring(path.LastIndexOf(",", StringComparison.Ordinal) + 1)) + // : int.Parse(path); + //} + /// /// Checks in a set of permissions associated with a user for those related to a given nodeId /// diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index a3e5510941..2b9e0b1ed3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -135,11 +135,11 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) { var hasPropertiesContentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - content1 = MockedContent.CreateSimpleContent(hasPropertiesContentType); - + content1 = MockedContent.CreateSimpleContent(hasPropertiesContentType); + contentTypeRepository.AddOrUpdate(hasPropertiesContentType); - repository.AddOrUpdate(content1); - unitOfWork.Commit(); + repository.AddOrUpdate(content1); + unitOfWork.Commit(); } var versionDtos = new List(); @@ -167,7 +167,7 @@ namespace Umbraco.Tests.Persistence.Repositories VersionId = version, WriterUserId = 0, UpdateDate = versionDate, - TemplateId = content1.Template == null || content1.Template.Id <= 0 ? null : (int?) content1.Template.Id + TemplateId = content1.Template == null || content1.Template.Id <= 0 ? null : (int?)content1.Template.Id }); } @@ -188,7 +188,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(contentItem.Version, versionDtos.Single(x => x.Id == versionDtos.Max(y => y.Id)).VersionId); var allVersions = repository.GetAllVersions(content[0].Id); - var allKnownVersions = versionDtos.Select(x => x.VersionId).Union(new[]{ content1.Version }).ToArray(); + var allKnownVersions = versionDtos.Select(x => x.VersionId).Union(new[] { content1.Version }).ToArray(); Assert.IsTrue(allKnownVersions.ContainsAll(allVersions.Select(x => x.Version))); Assert.IsTrue(allVersions.Select(x => x.Version).ContainsAll(allKnownVersions)); } @@ -547,9 +547,9 @@ namespace Umbraco.Tests.Persistence.Repositories var userGroup = MockedUserGroup.CreateUserGroup("1"); repository.AddOrUpdate(userGroup); unitOfWork.Commit(); - } - - ContentTypeRepository contentTypeRepository; + } + + ContentTypeRepository contentTypeRepository; using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) { var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); @@ -563,7 +563,7 @@ namespace Umbraco.Tests.Persistence.Repositories unitOfWork.Commit(); // Act - repository.AssignEntityPermission(parentPage, 'A', new [] { 1 }); + repository.AssignEntityPermission(parentPage, 'A', new[] { 1 }); var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); repository.AddOrUpdate(childPage); unitOfWork.Commit(); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index b521172417..93202af102 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1458,6 +1458,178 @@ namespace Umbraco.Tests.Services Assert.That(contents.Any(), Is.False); } + [Test] + public void Ensures_Permissions_Are_Set_On_Copied_Entity_To_Parent_Without_Permissions() + { + // Arrange + var userGroup = MockedUserGroup.CreateUserGroup("1"); + ServiceContext.UserService.Save(userGroup); + + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + contentType.AllowedContentTypes = new List + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) + }; + ServiceContext.ContentTypeService.Save(contentType); + + var parentPage = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage); + ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { 1 }); + + var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); + ServiceContext.ContentService.Save(childPage); + + //Ok, now copy, what should happen is the childPage will not have any permissions assigned since it's new parent + //doesn't have any assigned + var parentPage2 = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage2); + + var copy = ServiceContext.ContentService.Copy(childPage, parentPage2.Id, false, true); + + //Re-get the permissions, since there was none assigned to the new parent, the child shouldn't have any directly assigned + var copyPermissions = ServiceContext.ContentService.GetPermissionsForEntity(copy); + Assert.AreEqual(0, copyPermissions.Count()); + } + + [Test] + public void Ensures_Permissions_Are_Set_On_Copied_Entity_To_Parent_With_Permissions() + { + // Arrange + var userGroup = MockedUserGroup.CreateUserGroup("1"); + ServiceContext.UserService.Save(userGroup); + + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + contentType.AllowedContentTypes = new List + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) + }; + ServiceContext.ContentTypeService.Save(contentType); + + var parentPage = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage); + ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { 1 }); + + var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); + ServiceContext.ContentService.Save(childPage); + + //Ok, now copy, what should happen is the childPage will have it's new parent permissions copied over + var parentPage2 = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage2); + ServiceContext.ContentService.AssignContentPermission(parentPage2, 'B', new[] { 1 }); + + var copy = ServiceContext.ContentService.Copy(childPage, parentPage2.Id, false, true); + + //Re-get the permissions, since there was none assigned to the new parent, the child shouldn't have any directly assigned + var copyPermissions = ServiceContext.ContentService.GetPermissionsForEntity(copy); + Assert.AreEqual(1, copyPermissions.Count()); + Assert.AreEqual("B", copyPermissions.Single().AssignedPermissions.First()); + } + + [Test] + public void Ensures_Permissions_Are_Set_On_Copied_Descendants_To_Parent_With_Permissions() + { + // Arrange + var userGroup = MockedUserGroup.CreateUserGroup("1"); + ServiceContext.UserService.Save(userGroup); + + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + contentType.AllowedContentTypes = new List + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) + }; + ServiceContext.ContentTypeService.Save(contentType); + + var parentPage = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage); + ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { 1 }); + + var childPage1 = MockedContent.CreateSimpleContent(contentType, "child1", parentPage); + ServiceContext.ContentService.Save(childPage1); + var childPage2 = MockedContent.CreateSimpleContent(contentType, "child2", childPage1); + ServiceContext.ContentService.Save(childPage2); + var childPage3 = MockedContent.CreateSimpleContent(contentType, "child3", childPage2); + ServiceContext.ContentService.Save(childPage3); + + //Ok, now copy, what should happen is the childPage will have it's new parent permissions copied over + var parentPage2 = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage2); + ServiceContext.ContentService.AssignContentPermission(parentPage2, 'B', new[] { 1 }); + + var copy = ServiceContext.ContentService.Copy(childPage1, parentPage2.Id, false, true); + + //Re-get the permissions, since there was none assigned to the new parent, the child shouldn't have any directly assigned + var copyPermissions = ServiceContext.ContentService.GetPermissionsForEntity(copy); + Assert.AreEqual(1, copyPermissions.Count()); + Assert.AreEqual("B", copyPermissions.Single().AssignedPermissions.First()); + + var descendants = ServiceContext.ContentService.GetDescendants(copy).ToArray(); + Assert.AreEqual(2, descendants.Length); + + foreach (var descendant in descendants) + { + var permissions = ServiceContext.ContentService.GetPermissionsForEntity(descendant); + Assert.AreEqual(1, permissions.Count()); + Assert.AreEqual("B", permissions.Single().AssignedPermissions.First()); + } + } + + [Test] + public void Ensures_Permissions_Are_Set_On_Descendants_When_Permissions_Added() + { + // Arrange + var userGroup = MockedUserGroup.CreateUserGroup("1"); + ServiceContext.UserService.Save(userGroup); + + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + contentType.AllowedContentTypes = new List + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) + }; + ServiceContext.ContentTypeService.Save(contentType); + + var parentPage = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage); + var childPage1 = MockedContent.CreateSimpleContent(contentType, "child1", parentPage); + ServiceContext.ContentService.Save(childPage1); + var childPage2 = MockedContent.CreateSimpleContent(contentType, "child2", childPage1); + ServiceContext.ContentService.Save(childPage2); + var childPage3 = MockedContent.CreateSimpleContent(contentType, "child3", childPage2); + ServiceContext.ContentService.Save(childPage3); + + //ensure there are no permissions on any node + + var permissions = ServiceContext.ContentService.GetPermissionsForEntity(parentPage); + Assert.AreEqual(0, permissions.Count()); + + var descendants = ServiceContext.ContentService.GetDescendants(parentPage).ToArray(); + Assert.AreEqual(3, descendants.Length); + + foreach (var descendant in descendants) + { + permissions = ServiceContext.ContentService.GetPermissionsForEntity(descendant); + Assert.AreEqual(0, permissions.Count()); + } + + //Ok, now assign permissions to the parent, all descenents should get these right? + ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { 1 }); + + //re-test + + permissions = ServiceContext.ContentService.GetPermissionsForEntity(parentPage); + Assert.AreEqual(1, permissions.Count()); + Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); + + descendants = ServiceContext.ContentService.GetDescendants(parentPage).ToArray(); + Assert.AreEqual(3, descendants.Length); + + foreach (var descendant in descendants) + { + permissions = ServiceContext.ContentService.GetPermissionsForEntity(descendant); + Assert.AreEqual(1, permissions.Count()); + Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); + } + } + [Test] public void Can_Empty_RecycleBin_With_Content_That_Has_All_Related_Data() { diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index b0ec03f754..d672229e6b 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -4,14 +4,10 @@ using System.Security.Cryptography; using System.Text; using NUnit.Framework; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers.Entities; using umbraco.BusinessLogic.Actions; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; -using umbraco.BusinessLogic.Actions; using Umbraco.Core; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -37,7 +33,7 @@ namespace Umbraco.Tests.Services } [Test] - public void UserService_Get_User_Permissions_For_Unassigned_Permission_Nodes() + public void Get_User_Permissions_For_Unassigned_Permission_Nodes() { // Arrange var userService = ServiceContext.UserService; @@ -55,17 +51,18 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.Save(content); // Act - var permissions = userService.GetPermissions(user, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); + var permissions = userService.GetPermissions(user, content[0].Id, content[1].Id, content[2].Id) + .ToArray(); //assert - Assert.AreEqual(3, permissions.Count()); - Assert.AreEqual(17, permissions.ElementAt(0).AssignedPermissions.Count()); - Assert.AreEqual(17, permissions.ElementAt(1).AssignedPermissions.Count()); - Assert.AreEqual(17, permissions.ElementAt(2).AssignedPermissions.Count()); + Assert.AreEqual(3, permissions.Length); + Assert.AreEqual(17, permissions[0].AssignedPermissions.Length); + Assert.AreEqual(17, permissions[1].AssignedPermissions.Length); + Assert.AreEqual(17, permissions[2].AssignedPermissions.Length); } [Test] - public void UserService_Get_User_Permissions_For_Assigned_Permission_Nodes() + public void Get_User_Permissions_For_Assigned_Permission_Nodes() { // Arrange var userService = ServiceContext.UserService; @@ -99,7 +96,7 @@ namespace Umbraco.Tests.Services } [Test] - public void UserService_Get_UserGroup_Assigned_Permissions() + public void Get_UserGroup_Assigned_Permissions() { // Arrange var userService = ServiceContext.UserService; @@ -121,38 +118,6 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); ServiceContext.ContentService.AssignContentPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - // Act - var permissions = userService.GetPermissions(userGroup, true, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); - - //assert - Assert.AreEqual(3, permissions.Count()); - Assert.AreEqual(3, permissions.ElementAt(0).AssignedPermissions.Length); - Assert.AreEqual(2, permissions.ElementAt(1).AssignedPermissions.Length); - Assert.AreEqual(1, permissions.ElementAt(2).AssignedPermissions.Length); - } - - [Test] - public void UserService_Get_UserGroup_Assigned_And_Default_Permissions() - { - // Arrange - var userService = ServiceContext.UserService; - var userGroup = CreateTestUserGroup(); - - var contentType = MockedContentTypes.CreateSimpleContentType(); - ServiceContext.ContentTypeService.Save(contentType); - var content = new[] - { - MockedContent.CreateSimpleContent(contentType), - MockedContent.CreateSimpleContent(contentType), - MockedContent.CreateSimpleContent(contentType) - }; - ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - // Act var permissions = userService.GetPermissions(userGroup, false, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); @@ -160,7 +125,69 @@ namespace Umbraco.Tests.Services Assert.AreEqual(3, permissions.Count()); Assert.AreEqual(3, permissions.ElementAt(0).AssignedPermissions.Length); Assert.AreEqual(2, permissions.ElementAt(1).AssignedPermissions.Length); - Assert.AreEqual(17, permissions.ElementAt(2).AssignedPermissions.Length); + Assert.AreEqual(1, permissions.ElementAt(2).AssignedPermissions.Length); + } + + [Test] + public void Get_UserGroup_Assigned_And_Default_Permissions() + { + // Arrange + var userService = ServiceContext.UserService; + var userGroup = CreateTestUserGroup(); + + var contentType = MockedContentTypes.CreateSimpleContentType(); + ServiceContext.ContentTypeService.Save(contentType); + var content = new[] + { + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType) + }; + ServiceContext.ContentService.Save(content); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + + // Act + var permissions = userService.GetPermissions(userGroup, true, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id) + .ToArray(); + + //assert + Assert.AreEqual(3, permissions.Length); + Assert.AreEqual(3, permissions[0].AssignedPermissions.Length); + Assert.AreEqual(2, permissions[1].AssignedPermissions.Length); + Assert.AreEqual(17,permissions[2].AssignedPermissions.Length); + } + + [Test] + public void Get_User_Implicit_Permissions() + { + // Arrange + var userService = ServiceContext.UserService; + var userGroup = CreateTestUserGroup(); + + var contentType = MockedContentTypes.CreateSimpleContentType(); + ServiceContext.ContentTypeService.Save(contentType); + var parent = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parent); + var child1 = MockedContent.CreateSimpleContent(contentType, "child1", parent); + ServiceContext.ContentService.Save(child1); + var child2 = MockedContent.CreateSimpleContent(contentType, "child2", child1); + ServiceContext.ContentService.Save(child2); + + ServiceContext.ContentService.AssignContentPermission(parent, ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(parent, ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(parent, ActionMove.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(parent, ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(parent, ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + + // Act + var permissions = userService.GetPermissionsForPath(userGroup, child2.Path); + + //assert + Assert.AreEqual(3, permissions.AssignedPermissions.Length); } [Test] diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs index aaab898ae2..ee93aec60b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs @@ -89,7 +89,7 @@ namespace umbraco.dialogs if (a.CanBePermissionAssigned == false) continue; var permissions = userService.GetPermissionsForPath(group, _node.Path); - if (permissions.Contains(a.Letter)) + if (permissions.AssignedPermissions.Contains(a.Letter.ToString())) { chk.Checked = true; } diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index fbf226c39f..0f92a4427f 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -571,7 +571,8 @@ namespace umbraco.BusinessLogic if (_lazyId.HasValue) SetupUser(_lazyId.Value); var userService = ApplicationContext.Current.Services.UserService; - return userService.GetPermissionsForPath(UserEntity, path); + return string.Join("", + userService.GetPermissionsForPath(UserEntity, path).PermissionsSet.SelectMany(x => x.Permission)); } /// From d9937fb276c556e2b24517d18a770b891437f9dd Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 12 Jul 2017 14:01:36 +1000 Subject: [PATCH 372/714] Stop trying to build the UI.Client project as part of the sln build --- src/umbraco.sln | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/umbraco.sln b/src/umbraco.sln index df260903ee..a2603aecbe 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.26430.15 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}" EndProject @@ -127,9 +127,7 @@ Global {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.Build.0 = Release|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Release|Any CPU.ActiveCfg = Release|Any CPU From 7115d85440bc4d2d80a72f34df06f355a23d6525 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 12 Jul 2017 14:06:30 +1000 Subject: [PATCH 373/714] Removes unused and unneeded APIs on IUserService, updates code comments to describe implicit vs explicit permissions --- .../Interfaces/IUserGroupRepository.cs | 4 +- .../Repositories/UserGroupRepository.cs | 4 +- src/Umbraco.Core/Services/IUserService.cs | 35 +----- src/Umbraco.Core/Services/UserService.cs | 114 +++--------------- .../umbraco/dialogs/cruds.aspx.cs | 76 +----------- 5 files changed, 31 insertions(+), 202 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs index 3b2663e026..3ccfa89885 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs @@ -26,10 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[] userIds); /// - /// Gets the group permissions for the specified entities + /// Gets explicilty defined permissions for the group for specified entities /// /// Id of group - /// Array of entity Ids + /// Array of entity Ids, if empty will return permissions for the group for all entities IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds); /// diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index 7a160f7f24..e0717deda4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -91,10 +91,10 @@ namespace Umbraco.Core.Persistence.Repositories /// - /// Gets the group permissions for the specified entities + /// Gets explicilty defined permissions for the group for specified entities /// /// Id of group - /// Array of entity Ids + /// Array of entity Ids, if empty will return permissions for the group for all entities public IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds) { var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index b55048ef4d..de228412da 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -86,31 +86,19 @@ namespace Umbraco.Core.Services void DeleteSectionFromAllUserGroups(string sectionAlias); /// - /// Get permissions set for a user and optional node ids + /// Get explicitly assigned permissions for a user and optional node ids /// /// If no permissions are found for a particular entity then the user's default permissions will be applied /// User to retrieve permissions for /// Specifiying nothing will return all user permissions for all nodes /// An enumerable list of /// - /// > This is the ONLY one for permissions from 7.6! + /// This will return the default permissions for the user's groups for nodes that don't have explicitly defined permissions /// IEnumerable GetPermissions(IUser user, params int[] nodeIds); - + /// - /// Get permissions set for a group and optional node Ids - /// - /// Group to retrieve permissions for - /// - /// Flag indicating if we want to get just the permissions directly assigned for the group and path, - /// or fall back to the group's default permissions when nothing is directly assigned - /// - /// Specifiying nothing will return all permissions for all nodes - /// An enumerable list of - IEnumerable GetPermissions(string groupAlias, bool fallbackToDefaultPermissions, params int[] nodeIds); - - /// - /// Get permissions set for a group and optional node Ids + /// Get explicitly assigned permissions for a group and optional node Ids /// /// Group to retrieve permissions for /// @@ -122,23 +110,12 @@ namespace Umbraco.Core.Services IEnumerable GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); /// - /// Gets the permissions for the provided user and path + /// Gets the implicit/inherited permissions for the user for the given path /// /// User to check permissions for /// Path to check permissions for EntityPermissionSet GetPermissionsForPath(IUser user, string path); - - /// - /// Gets the permissions for the provided group and path - /// - /// Group alias to check permissions for - /// Path to check permissions for - /// - /// Flag indicating if we want to get just the permissions directly assigned for the group and path, - /// or fall back to the group's default permissions when nothing is directly assigned - /// - EntityPermission GetPermissionsForPath(string groupAlias, string path, bool fallbackToDefaultPermissions = false); - + /// /// Gets the permissions for the provided group and path /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index b2a1ddec58..841a51456e 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -13,7 +13,6 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Security; -using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { @@ -841,16 +840,13 @@ namespace Umbraco.Core.Services } /// - /// Get permissions set for a user and node Id + /// Get explicitly assigned permissions for a user and optional node ids /// /// User to retrieve permissions for /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of public IEnumerable GetPermissions(IUser user, params int[] nodeIds) { - if (nodeIds.Length == 0) - return Enumerable.Empty(); - var result = new List(); foreach (var group in user.Groups) @@ -859,35 +855,21 @@ namespace Umbraco.Core.Services { AddOrAmendPermissionList(result, permission); } - } - + } + return result; - } + } /// - /// Get permissions set for a group and node Id + /// Get explicitly assigned permissions for a group and optional node Ids /// - /// Group to retrieve permissions for + /// Group to retrieve permissions for /// /// Flag indicating if we want to get just the permissions directly assigned for the group and path, /// or fall back to the group's default permissions when nothing is directly assigned /// /// Specifiying nothing will return all permissions for all nodes - /// An enumerable list of - public IEnumerable GetPermissions(string groupAlias, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (nodeIds.Length == 0) - return Enumerable.Empty(); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateUserGroupRepository(uow); - var group = repository.Get(groupAlias); - if (group == null) throw new InvalidOperationException("No group found with alias " + groupAlias); - return GetPermissionsInternal(repository, group.ToReadOnlyGroup(), fallbackToDefaultPermissions, nodeIds); - } - } - + /// An enumerable list of private IEnumerable GetPermissions(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) { if (group == null) throw new ArgumentNullException("group"); @@ -899,31 +881,10 @@ namespace Umbraco.Core.Services var repository = RepositoryFactory.CreateUserGroupRepository(uow); return GetPermissionsInternal(repository, group, fallbackToDefaultPermissions, nodeIds); } - } - - private IEnumerable GetPermissions(int groupId, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (nodeIds.Length == 0) - return Enumerable.Empty(); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateUserGroupRepository(uow); + } - if (fallbackToDefaultPermissions == false) - { - //if fallbackToDefaultPermissions is false, we don't have to lookup the group - return repository.GetPermissionsForEntities(groupId, nodeIds); - } - - var group = repository.Get(groupId); - if (group == null) throw new InvalidOperationException("No group found with id " + groupId); - return GetPermissionsInternal(repository, group.ToReadOnlyGroup(), true, nodeIds); - } - } - /// - /// Get permissions set for a group and optional node Ids + /// Get explicitly assigned permissions for a group and optional node Ids /// /// Group to retrieve permissions for /// @@ -945,10 +906,7 @@ namespace Umbraco.Core.Services private static IEnumerable GetPermissionsInternal(IUserGroupRepository repository, IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) { if (group == null) throw new ArgumentNullException("group"); - - if (nodeIds.Length == 0) - return Enumerable.Empty(); - + var explicitPermissions = repository.GetPermissionsForEntities(group.Id, nodeIds); var result = new List(explicitPermissions); @@ -972,7 +930,7 @@ namespace Umbraco.Core.Services /// /// List of already found permissions /// New permission to aggregate - private void AddOrAmendPermissionList(IList permissions, EntityPermission groupPermission) + private static void AddOrAmendPermissionList(ICollection permissions, EntityPermission groupPermission) { //TODO: Fix the performance of this, we need to use things like HashSet and equality checkers, we are iterating too much @@ -988,7 +946,7 @@ namespace Umbraco.Core.Services } /// - /// Gets the permissions for the provided user and path + /// Gets the implicit/inherited permissions for the user for the given path /// /// User to check permissions for /// Path to check permissions for @@ -997,13 +955,13 @@ namespace Umbraco.Core.Services var nodeIds = GetIdsFromPath(path); if (nodeIds.Length == 0) - return null; + return EntityPermissionSet.Empty(); var permissionsByGroup = GetPermissionsForGroupsAndPath(user.Groups, nodeIds); // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? if (permissionsByGroup.Count == 0) - return null; + return EntityPermissionSet.Empty(); var entityId = nodeIds[0]; @@ -1036,25 +994,7 @@ namespace Umbraco.Core.Services permissions = GetPermissionsForPath(g, pathIds, fallbackToDefaultPermissions: true) }) .ToDictionary(x => x.group, x => x.permissions); - } - - /// - /// Gets the permissions for the provided group and path - /// - /// User to check permissions for - /// Path to check permissions for - /// - /// Flag indicating if we want to get just the permissions directly assigned for the group and path, - /// or fall back to the group's default permissions when nothing is directly assigned - /// - /// String indicating permissions for provided user and path - public EntityPermission GetPermissionsForPath(string groupAlias, string path, bool fallbackToDefaultPermissions = false) - { - var nodeIds = GetIdsFromPath(path); - //get permissions for all nodes in the path - var permissions = GetPermissions(groupAlias, fallbackToDefaultPermissions, nodeIds); - return GetPermissionsForPath(permissions, nodeIds, fallbackToDefaultPermissions); - } + } /// /// Gets the permissions for the provided group and path @@ -1078,15 +1018,7 @@ namespace Umbraco.Core.Services var permissions = GetPermissions(group, fallbackToDefaultPermissions, pathIds); return GetPermissionsForPath(permissions, pathIds, fallbackToDefaultPermissions); } - - //private EntityPermission GetPermissionsForPath(int groupId, string path, bool fallbackToDefaultPermissions = false) - //{ - // var nodeIds = GetIdsFromPath(path); - // //get permissions for all nodes in the path - // var permissions = GetPermissions(groupId, fallbackToDefaultPermissions, nodeIds); - // return GetPermissionsForPath(permissions, groupId, nodeIds, fallbackToDefaultPermissions); - //} - + /// /// Returns the permissions for the path ids /// @@ -1136,19 +1068,7 @@ namespace Umbraco.Core.Services .ToArray(); return nodeIds; } - - ///// - ///// Parses a path to find the lowermost node id - ///// - ///// Path as string - ///// Node id - //private static int GetNodeIdFromPath(string path) - //{ - // return path.Contains(",") - // ? int.Parse(path.Substring(path.LastIndexOf(",", StringComparison.Ordinal) + 1)) - // : int.Parse(path); - //} - + /// /// Checks in a set of permissions associated with a user for those related to a given nodeId /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs index ee93aec60b..de4c902ee2 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs @@ -19,7 +19,8 @@ namespace umbraco.dialogs /// /// Summary description for cruds. /// - public partial class cruds : BasePages.UmbracoEnsuredPage + [Obsolete("Remove this for 7.7 release!")] + public class cruds : BasePages.UmbracoEnsuredPage { public cruds() @@ -37,78 +38,9 @@ namespace umbraco.dialogs pane_form.Text = ui.Text("actions", "SetPermissionsForThePage",_node.Text); } - override protected void OnInit(EventArgs e) + protected override void OnInit(EventArgs e) { - base.OnInit(e); - - _node = new CMSNode(Request.GetItemAs("id")); - - var ht = new HtmlTable(); - ht.Attributes.Add("class", "table"); - - var names = new HtmlTableRow(); - - var corner = new HtmlTableCell("th"); - corner.Style.Add("border", "none"); - names.Cells.Add(corner); - - foreach (var a in ActionsResolver.Current.Actions) - { - if (a.CanBePermissionAssigned == false) continue; - - var permissionRow = new HtmlTableRow(); - var label = new HtmlTableCell - { - InnerText = ui.Text("actions", a.Alias) - }; - permissionRow.Cells.Add(label); - _permissions.Add(a.Alias, permissionRow); - } - - ht.Rows.Add(names); - - var userService = ApplicationContext.Current.Services.UserService; - foreach (var group in userService.GetAllUserGroups()) - { - var hc = new HtmlTableCell("th") - { - InnerText = group.Name - }; - hc.Style.Add("text-align", "center"); - hc.Style.Add("border", "none"); - names.Cells.Add(hc); - - foreach (var a in ActionsResolver.Current.Actions) - { - var chk = new CheckBox - { - //Each checkbox is named with the group _ permission alias so we can parse - ID = group.Id + "_" + a.Letter - }; - - if (a.CanBePermissionAssigned == false) continue; - - var permissions = userService.GetPermissionsForPath(group, _node.Path); - if (permissions.AssignedPermissions.Contains(a.Letter.ToString())) - { - chk.Checked = true; - } - - var cell = new HtmlTableCell(); - cell.Style.Add("text-align", "center"); - cell.Controls.Add(chk); - - _permissions[a.Alias].Cells.Add(cell); - } - } - - //add all collected rows - foreach (var perm in _permissions.Values) - { - ht.Rows.Add(perm); - } - - PlaceHolder1.Controls.Add(ht); + throw new NotSupportedException("This cruds.aspx.cs needs to be removed, it is no longer required"); } From 06f68b14172ac9c3f9ce05be45d57de0d1b26cd0 Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 12 Jul 2017 11:31:34 +0200 Subject: [PATCH 374/714] U4-10096 Ability to bulk assign users to groups in the user list --- .../src/common/resources/users.resource.js | 13 ++++ .../src/views/users/user.controller.js | 13 +--- .../users/views/users/users.controller.js | 64 +++++++++++++++++-- .../src/views/users/views/users/users.html | 42 ++++++------ src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 9 +++ .../umbraco/config/lang/en_us.xml | 6 +- src/Umbraco.Web/Editors/UsersController.cs | 17 +++++ 7 files changed, 122 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 706142dd18..0acd30afe0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -55,6 +55,18 @@ 'Failed to enable the users ' + userIds.join(",")); } + function setUserGroupsOnUsers(userGroups, userIds) { + var userGroupAliases = userGroups.map(function(o) { return o.alias; }); + var query = "userGroupAliases=" + userGroupAliases.join("&userGroupAliases=") + "&userIds=" + userIds.join("&userIds="); + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostSetUserGroupsOnUsers", + query)), + 'Failed to set user groups ' + userGroupAliases.join(",") + ' on the users ' + userIds.join(",")); + } + function getPagedResults(options) { var defaults = { pageSize: 25, @@ -173,6 +185,7 @@ var resource = { disableUsers: disableUsers, enableUsers: enableUsers, + setUserGroupsOnUsers: setUserGroupsOnUsers, getPagedResults: getPagedResults, getUser: getUser, createUser: createUser, diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index acc8c781dd..07539b79d2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -22,7 +22,7 @@ vm.enableUser = enableUser; vm.clearAvatar = clearAvatar; vm.save = save; - vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB" + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); vm.toggleChangePassword = toggleChangePassword; vm.emailIsUsername = true; @@ -56,8 +56,7 @@ vm.loading = false; }); - - }); + }); } function toggleChangePassword() { @@ -91,13 +90,9 @@ //the user has a password if they are not states: Invited, NoCredentials vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; - }, function (err) { - vm.page.saveButtonState = "error"; - }); - } function goToPage(ancestor) { @@ -303,7 +298,6 @@ }); } - function makeBreadcrumbs() { vm.breadcrumbs = [ { @@ -322,9 +316,6 @@ } init(); - } - angular.module("umbraco").controller("Umbraco.Editors.Users.UserController", UserEditController); - })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 67b4faf236..24e5e099d0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -24,6 +24,8 @@ vm.newUser.userGroups = []; vm.usersViewState = 'overview'; + vm.selectedBulkUserGroups = []; + vm.allowDisableUser = true; vm.allowEnableUser = true; vm.allowSetUserGroup = true; @@ -73,6 +75,7 @@ vm.clickUser = clickUser; vm.disableUsers = disableUsers; vm.enableUsers = enableUsers; + vm.openBulkUserGroupPicker = openBulkUserGroupPicker; vm.openUserGroupPicker = openUserGroupPicker; vm.removeSelectedUserGroup = removeSelectedUserGroup; vm.selectAll = selectAll; @@ -216,13 +219,47 @@ } function getUserFromArrayById(userId, users) { - var userFound; - angular.forEach(users, function(user){ - if(userId === user.id) { - userFound = user; + return _.find(users, function(u) { return u.id === userId }); + } + + function openBulkUserGroupPicker(event) { + var firstSelectedUser = getUserFromArrayById(vm.selection[0], vm.users); + + vm.selectedBulkUserGroups = _.clone(firstSelectedUser.userGroups); + + vm.userGroupPicker = { + title: "Select user groups", + view: "usergrouppicker", + selection: vm.selectedBulkUserGroups, + closeButtonLabel: "Cancel", + show: true, + submit: function (model) { + usersResource.setUserGroupsOnUsers(model.selection, vm.selection).then(function (data) { + // sorting to ensure they show up in right order when updating the UI + vm.selectedBulkUserGroups.sort(function (a, b) { + return a.alias > b.alias ? 1 : a.alias < b.alias ? -1 : 0; + }); + // apply changes to UI + _.each(vm.selection, + function(userId) { + var user = getUserFromArrayById(userId, vm.users); + user.userGroups = vm.selectedBulkUserGroups; + }); + vm.selectedBulkUserGroups = []; + vm.userGroupPicker.show = false; + vm.userGroupPicker = null; + formHelper.showNotifications(data); + clearSelection(); + }, function (error) { + formHelper.showNotifications(error.data); + }); + }, + close: function (oldModel) { + vm.selectedBulkUserGroups = []; + vm.userGroupPicker.show = false; + vm.userGroupPicker = null; } - }); - return userFound; + }; } function openUserGroupPicker(event) { @@ -487,6 +524,8 @@ vm.allowEnableUser = true; vm.allowSetUserGroup = true; + var firstSelectedUserGroups; + angular.forEach(users, function (user) { if (!user.selected) { @@ -513,6 +552,19 @@ vm.allowEnableUser = false; } + // store the user group aliases of the first selected user + if (!firstSelectedUserGroups) { + firstSelectedUserGroups = user.userGroups.map(function (ug) { return ug.alias; }); + vm.allowSetUserGroup = true; + } else if (vm.allowSetUserGroup === true) { + // for 2nd+ selected user, compare the user group aliases to determine if we should allow bulk editing. + // we don't allow bulk editing of users not currently having the same assigned user groups, as we can't + // really support that in the user group picker. + var userGroups = user.userGroups.map(function(ug) { return ug.alias; }); + if (_.difference(firstSelectedUserGroups, userGroups).length > 0) { + vm.allowSetUserGroup = false; + } + } }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 0293833f76..4490b33e43 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -47,7 +47,7 @@ @@ -60,32 +60,32 @@
- +
- +
- +
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 717c27270c..a748e63e28 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1126,6 +1126,15 @@ To manage your website, simply open the Umbraco back office and start adding con Script view not saved An error occurred saving the file. An error occurred saving the file. + Enabled %0% users + An error occurred while enabling the users + Disabled %0% users + An error occurred while disabling the users + %0% is now enabled + An error occurred while enabling the user + %0% is now disabled + An error occurred while disabling the user + User groups have been set Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 8620a5c75f..0a48f80b09 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1095,17 +1095,15 @@ To manage your website, simply open the Umbraco back office and start adding con Script view not saved An error occurred saving the file. An error occurred saving the file. - Enabled %0% users An error occurred while enabling the users Disabled %0% users An error occurred while disabling the users - - %0% is now enabled + %0% is now enabled An error occurred while enabling the user %0% is now disabled An error occurred while disabling the user - + User groups have been set Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 2625568c3e..58a5cdf119 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -546,6 +546,23 @@ namespace Umbraco.Web.Editors Services.TextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name })); } + public HttpResponseMessage PostSetUserGroupsOnUsers([FromUri]string[] userGroupAliases, [FromUri]int[] userIds) + { + var users = Services.UserService.GetUsersById(userIds).ToArray(); + var userGroups = Services.UserService.GetUserGroupsByAlias(userGroupAliases).Select(x => new ReadOnlyUserGroup(x.Id, x.Name, x.Icon, x.StartContentId, x.StartMediaId, x.Alias, x.AllowedSections)).ToArray(); + foreach (var u in users) + { + u.ClearGroups(); + foreach (var userGroup in userGroups) + { + u.AddGroup(userGroup); + } + } + Services.UserService.Save(users); + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/setUserGroupOnUsersSuccess")); + } + public class PagedUserResult : PagedResult { public PagedUserResult(long totalItems, long pageNumber, long pageSize) : base(totalItems, pageNumber, pageSize) From 44bb7e84dbf2da94de4267e5b6e4c911839c51a3 Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 12 Jul 2017 11:52:08 +0200 Subject: [PATCH 375/714] U4-10142 Adding a user - does not show up until refresh --- .../src/views/users/views/users/users.controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 67b4faf236..881c4931ff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -392,6 +392,7 @@ vm.page.createButtonState = "success"; vm.newUser = saved; setUsersViewState('createUserSuccess'); + getUsers(); }, function (err) { formHelper.handleError(err); vm.page.createButtonState = "error"; From f18b7110c9b89f5fd3e192d6a036c6fdc5c29682 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 12 Jul 2017 12:26:25 +0200 Subject: [PATCH 376/714] Minor fix in DataTypeValidateAttribute --- src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs index b15113fb66..ec7089d25d 100644 --- a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs +++ b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs @@ -43,7 +43,7 @@ namespace Umbraco.Web.Editors var dataType = (DataTypeSave)actionContext.ActionArguments["dataType"]; dataType.Name = dataType.Name.CleanForXss('[', ']', '(', ')', ':'); - dataType.Alias = dataType.Alias.CleanForXss('[', ']', '(', ')', ':'); + dataType.Alias = dataType.Alias == null ? dataType.Name : dataType.Alias.CleanForXss('[', ']', '(', ')', ':'); //Validate that the property editor exists var propertyEditor = PropertyEditorResolver.Current.GetByAlias(dataType.SelectedEditor); From 2e28ff3ccf5392e0fe96faa75a8af3e8d9f9f081 Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 12 Jul 2017 13:43:00 +0200 Subject: [PATCH 377/714] U4-10106 Cannot delete user groups --- .../src/common/resources/usergroups.resource.js | 14 +++++++++++++- .../views/users/views/groups/groups.controller.js | 15 ++++++++++++++- .../src/views/users/views/groups/groups.html | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 ++ src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 3 ++- src/Umbraco.Web/Editors/UserGroupsController.cs | 15 ++++++++++++++- 6 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/usergroups.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/usergroups.resource.js index fad2820f56..90d9e58736 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/usergroups.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/usergroups.resource.js @@ -59,11 +59,23 @@ "Failed to retrieve user groups"); } + function deleteUserGroups(userGroupIds) { + var query = "userGroupIds=" + userGroupIds.join("&userGroupIds="); + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userGroupsApiBaseUrl", + "PostDeleteUserGroups", + query)), + 'Failed to delete user groups'); + } + var resource = { saveUserGroup: saveUserGroup, getUserGroup: getUserGroup, getUserGroups: getUserGroups, - getUserGroupScaffold: getUserGroupScaffold + getUserGroupScaffold: getUserGroupScaffold, + deleteUserGroups: deleteUserGroups }; return resource; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index f10cd8e530..5c30f1354e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserGroupsController($scope, $timeout, $location, userGroupsResource) { + function UserGroupsController($scope, $timeout, $location, userGroupsResource, formHelper) { var vm = this; @@ -12,6 +12,7 @@ vm.clickUserGroup = clickUserGroup; vm.clearSelection = clearSelection; vm.selectUserGroup = selectUserGroup; + vm.deleteUserGroups = deleteUserGroups; function onInit() { @@ -57,6 +58,18 @@ } } + function deleteUserGroups() { + if (vm.selection.length > 0) { + userGroupsResource.deleteUserGroups(vm.selection).then(function (data) { + clearSelection(); + onInit(); + formHelper.showNotifications(data); + }, function(error) { + formHelper.showNotifications(error.data); + }); + } + } + function clearSelection() { angular.forEach(vm.userGroups, function (userGroup) { userGroup.selected = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 738fc4f654..38dc7a77f6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -32,7 +32,7 @@ type="button" label="Delete" icon="icon-trash" - action="vm.deleteUserGroup()" + action="vm.deleteUserGroups()" size="xs">
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 717c27270c..53c11eb420 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1126,6 +1126,8 @@ To manage your website, simply open the Umbraco back office and start adding con Script view not saved An error occurred saving the file. An error occurred saving the file. + Deleted %0% user groups + %0% was deleted Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 8620a5c75f..f4e00c52a9 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1105,7 +1105,8 @@ To manage your website, simply open the Umbraco back office and start adding con An error occurred while enabling the user %0% is now disabled An error occurred while disabling the user - + Deleted %0% user groups + %0% was deleted Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index 43e7810227..9a1e26007f 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -5,7 +5,6 @@ using System.Net; using System.Net.Http; using System.Web.Http; using AutoMapper; -using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; @@ -87,5 +86,19 @@ namespace Umbraco.Web.Editors return display; } + + public HttpResponseMessage PostDeleteUserGroups([FromUri] int[] userGroupIds) + { + var userGroups = Services.UserService.GetAllUserGroups(userGroupIds).ToArray(); + foreach (var userGroup in userGroups) + { + Services.UserService.DeleteUserGroup(userGroup); + } + if (userGroups.Length > 1) + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/deleteUserGroupSuccess", new[] {userGroups[0].Name})); + } } } \ No newline at end of file From c8828b968c935baee36844fd13b5b4cafd7011f3 Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 12 Jul 2017 13:56:18 +0200 Subject: [PATCH 378/714] found an extension method :) --- src/Umbraco.Web/Editors/UsersController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 58a5cdf119..0039128e86 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -549,7 +549,7 @@ namespace Umbraco.Web.Editors public HttpResponseMessage PostSetUserGroupsOnUsers([FromUri]string[] userGroupAliases, [FromUri]int[] userIds) { var users = Services.UserService.GetUsersById(userIds).ToArray(); - var userGroups = Services.UserService.GetUserGroupsByAlias(userGroupAliases).Select(x => new ReadOnlyUserGroup(x.Id, x.Name, x.Icon, x.StartContentId, x.StartMediaId, x.Alias, x.AllowedSections)).ToArray(); + var userGroups = Services.UserService.GetUserGroupsByAlias(userGroupAliases).Select(x => x.ToReadOnlyGroup()).ToArray(); foreach (var u in users) { u.ClearGroups(); From 044120ffd2e62fbc6188a719dc3f900cba6810bf Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Jul 2017 00:07:51 +1000 Subject: [PATCH 379/714] Gets inherited permissions working, adds lots of unit tests, few more tests to write now. --- .../Models/Membership/ContentPermissionSet.cs | 54 ++++ .../Models/Membership/EntityPermission.cs | 11 +- .../Models/Membership/EntityPermissionSet.cs | 76 +++--- .../Membership/UserGroupEntityPermission.cs | 16 -- .../ContentBlueprintRepository.cs | 31 +++ .../Repositories/ContentRepository.cs | 75 ++---- .../Interfaces/IContentRepository.cs | 17 +- .../Interfaces/IUserGroupRepository.cs | 10 +- .../Repositories/PermissionRepository.cs | 254 ++++++++++++------ .../Repositories/SimpleGetRepository.cs | 10 +- .../Repositories/UserGroupRepository.cs | 39 ++- src/Umbraco.Core/Services/ContentService.cs | 18 +- .../Services/ContentServiceExtensions.cs | 2 +- src/Umbraco.Core/Services/IContentService.cs | 4 +- src/Umbraco.Core/Services/IUserService.cs | 10 +- src/Umbraco.Core/Services/UserService.cs | 196 +++++++------- src/Umbraco.Core/StringExtensions.cs | 16 ++ src/Umbraco.Core/Umbraco.Core.csproj | 3 +- .../Repositories/ContentRepositoryTest.cs | 43 +-- .../Services/ContentServiceTests.cs | 141 ++-------- .../Services/UserServiceTests.cs | 130 ++++++++- .../Controllers/ContentControllerUnitTests.cs | 41 +-- ...terAllowedOutgoingContentAttributeTests.cs | 8 +- .../Cache/CacheRefresherEventHandler.cs | 7 +- src/Umbraco.Web/Editors/ContentController.cs | 12 +- src/umbraco.businesslogic/User.cs | 2 +- 26 files changed, 725 insertions(+), 501 deletions(-) create mode 100644 src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs delete mode 100644 src/Umbraco.Core/Models/Membership/UserGroupEntityPermission.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/ContentBlueprintRepository.cs diff --git a/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs new file mode 100644 index 0000000000..1345be2c8c --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Membership +{ + /// + /// Represents an -> user group & permission key value pair collection + /// + /// + /// This implements purely so it can be used with the repository layer which is why it's explicitly implemented. + /// + public class ContentPermissionSet : EntityPermissionSet, IAggregateRoot + { + private readonly IContent _content; + + public ContentPermissionSet(IContent content, IEnumerable permissionsSet) + : base(content.Id, permissionsSet) + { + _content = content; + } + + public override int EntityId + { + get { return _content.Id; } + } + + #region Explicit implementation of IAggregateRoot + int IEntity.Id + { + get { return EntityId; } + set { throw new NotImplementedException(); } + } + + bool IEntity.HasIdentity + { + get { return EntityId > 0; } + } + + Guid IEntity.Key { get; set; } + + DateTime IEntity.CreateDate { get; set; } + + DateTime IEntity.UpdateDate { get; set; } + + DateTime? IDeletableEntity.DeletedDate { get; set; } + + object IDeepCloneable.DeepClone() + { + throw new NotImplementedException(); + } + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs index a1fa57d274..c6d9e8350d 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs @@ -7,21 +7,24 @@ namespace Umbraco.Core.Models.Membership ///
public class EntityPermission { - public EntityPermission(int entityId, string[] assignedPermissions) + public EntityPermission(int groupId, int entityId, string[] assignedPermissions) { + UserGroupId = groupId; EntityId = entityId; AssignedPermissions = assignedPermissions; IsDefaultPermissions = false; } - public EntityPermission(int entityId, string[] assignedPermissions, bool isDefaultPermissions) + public EntityPermission(int groupId, int entityId, string[] assignedPermissions, bool isDefaultPermissions) { + UserGroupId = groupId; EntityId = entityId; AssignedPermissions = assignedPermissions; IsDefaultPermissions = isDefaultPermissions; } public int EntityId { get; private set; } + public int UserGroupId { get; private set; } /// /// The assigned permissions for the user/entity combo @@ -50,7 +53,9 @@ namespace Umbraco.Core.Models.Membership AssignedPermissions = newPermissions .Distinct() .ToArray(); - } + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs index d91cd3a352..dd9190ae85 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs @@ -1,60 +1,56 @@ using System.Collections.Generic; +using System.Linq; namespace Umbraco.Core.Models.Membership { /// /// Represents an entity -> user group & permission key value pair collection - /// + /// public class EntityPermissionSet { /// - /// The entity id with permissions assigned + /// Returns an empty permission set /// - public int EntityId { get; private set; } + /// + public static EntityPermissionSet Empty() + { + return new EntityPermissionSet(-1, new EntityPermission[0]); + } - /// - /// The key/value pairs of user group id & single permission - /// - public IEnumerable PermissionsSet { get; private set; } - - public EntityPermissionSet(int entityId, IEnumerable permissionsSet) + public EntityPermissionSet(int entityId, IEnumerable permissionsSet) { EntityId = entityId; PermissionsSet = permissionsSet; } - public class UserGroupPermission + /// + /// The entity id with permissions assigned + /// + public virtual int EntityId { get; private set; } + + /// + /// The key/value pairs of user group id & single permission + /// + public IEnumerable PermissionsSet { get; private set; } + + /// + /// Returns the aggregte permissions in the permission set + /// + /// + /// + /// This value is only calculated once + /// + public IEnumerable GetAllPermissions() { - public UserGroupPermission(int groupId, string permission) - { - UserGroupId = groupId; - Permission = permission; - } - - public int UserGroupId { get; private set; } - - public string Permission { get; private set; } - - protected bool Equals(UserGroupPermission other) - { - return UserGroupId == other.UserGroupId && string.Equals(Permission, other.Permission); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((UserGroupPermission) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (UserGroupId * 397) ^ Permission.GetHashCode(); - } - } + return _calculatedPermissions ?? (_calculatedPermissions = + PermissionsSet.SelectMany(x => x.AssignedPermissions).Distinct().ToArray()); } + + private string[] _calculatedPermissions; + + + + + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/UserGroupEntityPermission.cs b/src/Umbraco.Core/Models/Membership/UserGroupEntityPermission.cs deleted file mode 100644 index 619223f01a..0000000000 --- a/src/Umbraco.Core/Models/Membership/UserGroupEntityPermission.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Umbraco.Core.Models.Membership -{ - /// - /// Represents a user group -> entity permission - /// - public class UserGroupEntityPermission : EntityPermission - { - public UserGroupEntityPermission(int groupId, int entityId, string[] assignedPermissions) - : base(entityId, assignedPermissions) - { - UserGroupId = groupId; - } - - public int UserGroupId { get; private set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentBlueprintRepository.cs new file mode 100644 index 0000000000..4c9021d20a --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ContentBlueprintRepository.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Override the base content repository so we can change the node object type + /// + /// + /// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, however to do that + /// requires quite a lot of work since we'd need to re-organize the interhitance quite a lot or create a helper class to perform a lot of the underlying logic. + /// + /// TODO: Create a helper method to contain most of the underlying logic for the ContentRepository + /// + internal class ContentBlueprintRepository : ContentRepository + { + public ContentBlueprintRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cacheHelper, logger, syntaxProvider, contentTypeRepository, templateRepository, tagRepository, contentSection) + { + } + + protected override Guid NodeObjectTypeId + { + get { return Constants.ObjectTypes.DocumentBlueprintGuid; } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index f07e9c8385..2c3f126c6e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -20,28 +20,6 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - /// - /// Override the base content repository so we can change the node object type - /// - /// - /// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, however to do that - /// requires quite a lot of work since we'd need to re-organize the interhitance quite a lot or create a helper class to perform a lot of the underlying logic. - /// - /// TODO: Create a helper method to conain most of the underlying logic for the ContentRepository - /// - internal class ContentBlueprintRepository : ContentRepository - { - public ContentBlueprintRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cacheHelper, logger, syntaxProvider, contentTypeRepository, templateRepository, tagRepository, contentSection) - { - } - - protected override Guid NodeObjectTypeId - { - get { return Constants.ObjectTypes.DocumentBlueprintGuid; } - } - - } - /// /// Represents a repository for doing CRUD operations for /// @@ -50,9 +28,9 @@ namespace Umbraco.Core.Persistence.Repositories private readonly IContentTypeRepository _contentTypeRepository; private readonly ITemplateRepository _templateRepository; private readonly ITagRepository _tagRepository; - private readonly CacheHelper _cacheHelper; private readonly ContentPreviewRepository _contentPreviewRepository; private readonly ContentXmlRepository _contentXmlRepository; + private readonly PermissionRepository _permissionRepository; public ContentRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cacheHelper, logger, syntaxProvider, contentSection) @@ -63,10 +41,10 @@ namespace Umbraco.Core.Persistence.Repositories _contentTypeRepository = contentTypeRepository; _templateRepository = templateRepository; _tagRepository = tagRepository; - _cacheHelper = cacheHelper; _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, syntaxProvider); _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, syntaxProvider); - + _permissionRepository = new PermissionRepository(UnitOfWork, cacheHelper, Logger, SqlSyntax); + EnsureUniqueNaming = true; } @@ -468,26 +446,7 @@ namespace Umbraco.Core.Persistence.Repositories entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set entity.Path = nodeDto.Path; entity.SortOrder = sortOrder; - entity.Level = level; - - ////Assign the same permissions to it as the parent node - //// http://issues.umbraco.org/issue/U4-2161 - //var permissionsRepo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - //var parentPermissions = permissionsRepo.GetPermissionsForEntity(entity.ParentId).ToArray(); - ////if there are parent permissions then assign them, otherwise leave null and permissions will become the - //// user's default permissions. - //if (parentPermissions.Length > 0) - //{ - // var userGroupPermissions = ( - // from perm in parentPermissions - // from p in perm.AssignedPermissions - // select new EntityPermissionSet.UserGroupPermission(perm.UserGroupId, p)).ToList(); - - // permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userGroupPermissions)); - // //flag the entity's permissions changed flag so we can track those changes. - // //Currently only used for the cache refreshers to detect if we should refresh all user permissions cache. - // ((Content)entity).PermissionsChanged = true; - //} + entity.Level = level; //Create the Content specific data - cmsContent var contentDto = dto.ContentVersionDto.ContentDto; @@ -863,8 +822,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", public void ReplaceContentPermissions(EntityPermissionSet permissionSet) { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.ReplaceEntityPermissions(permissionSet); + _permissionRepository.ReplaceEntityPermissions(permissionSet); } public void ClearPublished(IContent content) @@ -880,21 +838,19 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", /// /// public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.AssignEntityPermission(entity, permission, groupIds); + { + _permissionRepository.AssignEntityPermission(entity, permission, groupIds); } /// - /// Returns permissions directly assigned to the content item for all user groups + /// Gets the explicit list of permissions for the content item /// /// /// - public IEnumerable GetPermissionsForEntity(int entityId) + public IEnumerable GetPermissionsForEntity(int entityId) { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - return repo.GetPermissionsForEntity(entityId); - } + return _permissionRepository.GetPermissionsForEntity(entityId); + } /// /// Adds/updates content/published xml @@ -906,6 +862,15 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } + /// + /// Used to add/update a permission for a content item + /// + /// + public void AddOrUpdatePermissions(ContentPermissionSet permission) + { + _permissionRepository.AddOrUpdate(permission); + } + /// /// Used to remove the content xml for a content item /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 41889de1c4..6f643b3e0b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -57,11 +57,18 @@ namespace Umbraco.Core.Persistence.Repositories void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); /// - /// Gets the list of permissions for the content item + /// Gets the explicit list of permissions for the content item /// /// /// - IEnumerable GetPermissionsForEntity(int entityId); + IEnumerable GetPermissionsForEntity(int entityId); + + ///// + ///// Gets the implicit/inherited list of permissions for the content item + ///// + ///// + ///// + //IEnumerable GetPermissionsForPath(string path); /// /// Used to add/update published xml for the content item @@ -70,6 +77,12 @@ namespace Umbraco.Core.Persistence.Repositories /// void AddOrUpdateContentXml(IContent content, Func xml); + /// + /// Used to add/update a permission for a content item + /// + /// + void AddOrUpdatePermissions(ContentPermissionSet permission); + /// /// Used to remove the content xml for a content item /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs index 3ccfa89885..b018bd3c08 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs @@ -32,6 +32,14 @@ namespace Umbraco.Core.Persistence.Repositories /// Array of entity Ids, if empty will return permissions for the group for all entities IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds); + /// + /// Gets explicilt and default permissions (if requested) permissions for the group for specified entities + /// + /// The group + /// If true will include the group's default permissions if no permissions are explicitly assigned + /// Array of entity Ids, if empty will return permissions for the group for all entities + IEnumerable GetPermissionsForEntities(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); + /// /// Replaces the same permission set for a single group to any number of entities /// @@ -47,6 +55,6 @@ namespace Umbraco.Core.Persistence.Repositories /// Permissions as enumerable list of /// Specify the nodes to replace permissions for void AssignGroupPermission(int groupId, char permission, params int[] entityIds); - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index 5ae4efad45..4cad3be7e7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -12,6 +12,9 @@ using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using CacheKeys = Umbraco.Core.Cache.CacheKeys; using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { @@ -19,19 +22,18 @@ namespace Umbraco.Core.Persistence.Repositories /// A repository that exposes functionality to modify assigned permissions to a node /// /// - internal class PermissionRepository + /// + /// This repo implements the base class so that permissions can be queued to be persisted + /// like the normal repository pattern but the standard repository Get commands don't apply and will throw + /// + internal class PermissionRepository : PetaPocoRepositoryBase where TEntity : class, IAggregateRoot { - private readonly IScopeUnitOfWork _unitOfWork; - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly ISqlSyntaxProvider _sqlSyntax; - internal PermissionRepository(IScopeUnitOfWork unitOfWork, CacheHelper cache, ISqlSyntaxProvider sqlSyntax) + public PermissionRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) { - _unitOfWork = unitOfWork; - //Make this repository use an isolated cache - _runtimeCache = cache.IsolatedRuntimeCache.GetOrCreateCache(); - _sqlSyntax = sqlSyntax; + } /// @@ -41,7 +43,6 @@ namespace Umbraco.Core.Persistence.Repositories /// /// public IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds) - { //var whereCriteria = GetPermissionsForEntitiesCriteria(groupId, entityIds); var result = new List(); //iterate in groups of 2000 since we don't want to exceed the max SQL param count @@ -51,9 +52,9 @@ namespace Umbraco.Core.Persistence.Repositories var sql = new Sql(); sql.Select("*") - .From(_sqlSyntax) - .Where(dto => dto.UserGroupId == groupId && ids.Contains(dto.NodeId), _sqlSyntax); - var permissions = _unitOfWork.Database.Fetch(sql); + .From(SqlSyntax) + .Where(dto => dto.UserGroupId == groupId && ids.Contains(dto.NodeId), SqlSyntax); + var permissions = UnitOfWork.Database.Fetch(sql); result.AddRange(ConvertToPermissionList(permissions)); } return result; @@ -73,62 +74,62 @@ namespace Umbraco.Core.Persistence.Repositories // sql.Select("*") // .From() // .Where(whereCriteria); - // var result = _unitOfWork.Database.Fetch(sql); + // var result = UnitOfWork.Database.Fetch(sql); // return ConvertToPermissionList(result); // }, // GetCacheTimeout(), // priority: GetCachePriority()); - } - - //private static string GetEntityIdKey(int[] entityIds) - //{ - // return string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); - //} - - //private static TimeSpan GetCacheTimeout() - //{ - // //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will only have this exist in cache for 20 minutes, - // // then it will refresh from the database. - // return new TimeSpan(0, 20, 0); - //} - - //private static CacheItemPriority GetCachePriority() - //{ - // //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will make this priority below average - // return CacheItemPriority.BelowNormal; - //} - - private static IEnumerable ConvertToPermissionList(IEnumerable result) + } + + //private static string GetEntityIdKey(int[] entityIds) + //{ + // return string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); + //} + + //private static TimeSpan GetCacheTimeout() + //{ + // //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will only have this exist in cache for 20 minutes, + // // then it will refresh from the database. + // return new TimeSpan(0, 20, 0); + //} + + //private static CacheItemPriority GetCachePriority() + //{ + // //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will make this priority below average + // return CacheItemPriority.BelowNormal; + //} + + /// + /// Returns permissions directly assigned to the content items for all user groups + /// + /// + /// + public IEnumerable GetPermissionsForEntities(int[] entityIds) { - var permissions = new List(); - var nodePermissions = result.GroupBy(x => x.NodeId); - foreach (var np in nodePermissions) - { - var userGroupPermissions = np.GroupBy(x => x.UserGroupId); - foreach (var permission in userGroupPermissions) - { - var perms = permission.Select(x => x.Permission).ToArray(); - permissions.Add(new UserGroupEntityPermission(permission.Key, permission.First().NodeId, perms)); - } - } + var sql = new Sql(); + sql.Select("*") + .From(SqlSyntax) + .Where(dto => entityIds.Contains(dto.NodeId), SqlSyntax) + .OrderBy(dto => dto.NodeId, SqlSyntax); - return permissions; - } + var result = UnitOfWork.Database.Fetch(sql); + return ConvertToPermissionList(result); + } /// /// Returns permissions directly assigned to the content item for all user groups /// /// /// - public IEnumerable GetPermissionsForEntity(int entityId) + public IEnumerable GetPermissionsForEntity(int entityId) { var sql = new Sql(); sql.Select("*") - .From() - .Where(dto => dto.NodeId == entityId) - .OrderBy(dto => dto.NodeId); + .From(SqlSyntax) + .Where(dto => dto.NodeId == entityId, SqlSyntax) + .OrderBy(dto => dto.NodeId, SqlSyntax); - var result = _unitOfWork.Database.Fetch(sql); + var result = UnitOfWork.Database.Fetch(sql); return ConvertToPermissionList(result); } @@ -143,7 +144,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public void ReplacePermissions(int groupId, IEnumerable permissions, params int[] entityIds) { - var db = _unitOfWork.Database; + var db = UnitOfWork.Database; //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)"; @@ -166,10 +167,10 @@ namespace Umbraco.Core.Persistence.Repositories } } - _unitOfWork.Database.BulkInsertRecords(toInsert, _sqlSyntax); + UnitOfWork.Database.BulkInsertRecords(toInsert, SqlSyntax); //Raise the event - _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(toInsert), false)); + UnitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(toInsert), false)); } @@ -181,7 +182,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public void AssignPermission(int groupId, char permission, params int[] entityIds) { - var db = _unitOfWork.Database; + var db = UnitOfWork.Database; var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)"; db.Execute(sql, new @@ -198,10 +199,10 @@ namespace Umbraco.Core.Persistence.Repositories UserGroupId = groupId }).ToArray(); - _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); + UnitOfWork.Database.BulkInsertRecords(actions, SqlSyntax); //Raise the event - _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); + UnitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } @@ -213,7 +214,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) { - var db = _unitOfWork.Database; + var db = UnitOfWork.Database; var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)"; db.Execute(sql, new @@ -230,15 +231,15 @@ namespace Umbraco.Core.Persistence.Repositories UserGroupId = id }).ToArray(); - _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); + UnitOfWork.Database.BulkInsertRecords(actions, SqlSyntax); //Raise the event - _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); + UnitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } /// - /// Assigns permissions to an entity for multiple users/permission entries + /// Assigns permissions to an entity for multiple group/permission entries /// /// /// @@ -247,25 +248,118 @@ namespace Umbraco.Core.Persistence.Repositories /// public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) { - var db = _unitOfWork.Database; - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId"; - db.Execute(sql, new { nodeId = permissionSet.EntityId }); - - var actions = permissionSet.PermissionsSet.Select(p => new UserGroup2NodePermissionDto - { - NodeId = permissionSet.EntityId, - Permission = p.Permission, - UserGroupId = p.UserGroupId - }).ToArray(); + var db = UnitOfWork.Database; + var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId"; + db.Execute(sql, new { nodeId = permissionSet.EntityId }); + + var toInsert = new List(); + foreach (var entityPermission in permissionSet.PermissionsSet) + { + foreach (var permission in entityPermission.AssignedPermissions) + { + toInsert.Add(new UserGroup2NodePermissionDto + { + NodeId = permissionSet.EntityId, + Permission = permission, + UserGroupId = entityPermission.UserGroupId + }); + } + } + + UnitOfWork.Database.BulkInsertRecords(toInsert, SqlSyntax); + + //Raise the event + UnitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(permissionSet.PermissionsSet, false)); + - _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); - - //Raise the event - _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); - - } - public static event TypedEventHandler, SaveEventArgs> AssignedPermissions; + + #region Not implemented (don't need to for the purposes of this repo) + protected override ContentPermissionSet PerformGet(int id) + { + throw new NotImplementedException(); + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + throw new NotImplementedException(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + throw new NotImplementedException(); + } + + protected override string GetBaseWhereClause() + { + throw new NotImplementedException(); + } + + protected override IEnumerable GetDeleteClauses() + { + return new List(); + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistDeletedItem(ContentPermissionSet entity) + { + throw new NotImplementedException(); + } + + #endregion + + /// + /// Used to add or update entity permissions during a content item being updated + /// + /// + protected override void PersistNewItem(ContentPermissionSet entity) + { + //does the same thing as update + PersistUpdatedItem(entity); + } + + /// + /// Used to add or update entity permissions during a content item being updated + /// + /// + protected override void PersistUpdatedItem(ContentPermissionSet entity) + { + var asAggregateRoot = (IAggregateRoot)entity; + if (asAggregateRoot.HasIdentity == false) + { + throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); + } + + ReplaceEntityPermissions(entity); + } + + private static IEnumerable ConvertToPermissionList(IEnumerable result) + { + var permissions = new List(); + var nodePermissions = result.GroupBy(x => x.NodeId); + foreach (var np in nodePermissions) + { + var userGroupPermissions = np.GroupBy(x => x.UserGroupId); + foreach (var permission in userGroupPermissions) + { + var perms = permission.Select(x => x.Permission).ToArray(); + permissions.Add(new EntityPermission(permission.Key, np.Key, perms)); + } + } + + return permissions; + } + + public static event TypedEventHandler, SaveEventArgs> AssignedPermissions; } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs index 67c9149422..28e676743f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs @@ -66,7 +66,7 @@ namespace Umbraco.Core.Persistence.Repositories return Database.Fetch(sql).Select(ConvertToEntity); } - protected override sealed IEnumerable PerformGetByQuery(IQuery query) + protected sealed override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); @@ -76,22 +76,22 @@ namespace Umbraco.Core.Persistence.Repositories #region Not implemented and not required - protected override sealed IEnumerable GetDeleteClauses() + protected sealed override IEnumerable GetDeleteClauses() { throw new NotImplementedException(); } - protected override sealed Guid NodeObjectTypeId + protected sealed override Guid NodeObjectTypeId { get { throw new NotImplementedException(); } } - protected override sealed void PersistNewItem(TEntity entity) + protected sealed override void PersistNewItem(TEntity entity) { throw new NotImplementedException(); } - protected override sealed void PersistUpdatedItem(TEntity entity) + protected sealed override void PersistUpdatedItem(TEntity entity) { throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index e0717deda4..bd2b8390cd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -22,12 +22,14 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly CacheHelper _cacheHelper; private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; + private readonly PermissionRepository _permissionRepository; public UserGroupRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cacheHelper, logger, sqlSyntax) { _cacheHelper = cacheHelper; _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, work, cacheHelper, logger, sqlSyntax); + _permissionRepository = new PermissionRepository(work, _cacheHelper, logger, sqlSyntax); } public const string GetByAliasCacheKeyPrefix = "UserGroupRepository_GetByAlias_"; @@ -97,8 +99,33 @@ namespace Umbraco.Core.Persistence.Repositories /// Array of entity Ids, if empty will return permissions for the group for all entities public IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds) { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - return repo.GetPermissionsForEntities(groupId, entityIds); + return _permissionRepository.GetPermissionsForEntities(groupId, entityIds); + } + + /// + /// Gets explicilt and default permissions (if requested) permissions for the group for specified entities + /// + /// The group + /// If true will include the group's default permissions if no permissions are explicitly assigned + /// Array of entity Ids, if empty will return permissions for the group for all entities + public IEnumerable GetPermissionsForEntities(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (group == null) throw new ArgumentNullException("group"); + + var explicitPermissions = GetPermissionsForEntities(group.Id, nodeIds); + var result = new List(explicitPermissions); + + // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults + if (fallbackToDefaultPermissions) + { + var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToArray(); + if (missingIds.Length > 0) + { + result.AddRange(missingIds + .Select(i => new EntityPermission(group.Id, i, group.Permissions.ToArray(), isDefaultPermissions: true))); + } + } + return result; } /// @@ -109,8 +136,7 @@ namespace Umbraco.Core.Persistence.Repositories /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. public void ReplaceGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds) { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.ReplacePermissions(groupId, permissions, entityIds); + _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); } /// @@ -121,9 +147,8 @@ namespace Umbraco.Core.Persistence.Repositories /// Specify the nodes to replace permissions for public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.AssignPermission(groupId, permission, entityIds); - } + _permissionRepository.AssignPermission(groupId, permission, entityIds); + } #region Overrides of RepositoryBase diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 6e09d5f845..b7649ea382 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -130,11 +130,11 @@ namespace Umbraco.Core.Services } /// - /// Returns permissions directly assigned to the content item for all user groups + /// Returns implicit/inherited permissions assigned to the content item for all user groups /// /// /// - public IEnumerable GetPermissionsForEntity(IContent content) + public IEnumerable GetPermissionsForEntity(IContent content) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { @@ -1671,9 +1671,21 @@ namespace Umbraco.Core.Services // Update the create author and last edit author copy.CreatorId = userId; copy.WriterId = userId; + + //get the current permissions, if there are any explicit ones they need to be copied + var currentPermissions = GetPermissionsForEntity(content) + .Where(x => x.IsDefaultPermissions == false).ToArray(); repository.AddOrUpdate(copy); - repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + //add permissions + if (currentPermissions.Length > 0) + { + var permissionSet = new ContentPermissionSet(copy, currentPermissions); + repository.AddOrUpdatePermissions(permissionSet); + } + uow.Commit(); // todo - this should flush, not commit //Special case for the associated tags diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index 95fe79f9ac..c6dc0274b6 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Services /// public static void RemoveContentPermissions(this IContentService contentService, int contentId) { - contentService.ReplaceContentPermissions(new EntityPermissionSet(contentId, Enumerable.Empty())); + contentService.ReplaceContentPermissions(new EntityPermissionSet(contentId, Enumerable.Empty())); } /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 289af7324b..251bf169ad 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -151,11 +151,11 @@ namespace Umbraco.Core.Services void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds); /// - /// Returns permissions directly assigned to the content item for all user groups + /// Returns implicit/inherited permissions assigned to the content item for all user groups /// /// /// - IEnumerable GetPermissionsForEntity(IContent content); + IEnumerable GetPermissionsForEntity(IContent content); bool SendToPublication(IContent content, int userId = 0); diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index de228412da..c5b282cd66 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -102,8 +102,7 @@ namespace Umbraco.Core.Services /// /// Group to retrieve permissions for /// - /// Flag indicating if we want to get just the permissions directly assigned for the group and path, - /// or fall back to the group's default permissions when nothing is directly assigned + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of @@ -114,16 +113,15 @@ namespace Umbraco.Core.Services /// /// User to check permissions for /// Path to check permissions for - EntityPermissionSet GetPermissionsForPath(IUser user, string path); - + EntityPermissionSet GetPermissionsForPath(IUser user, string path); + /// /// Gets the permissions for the provided group and path /// /// Group to check permissions for /// Path to check permissions for /// - /// Flag indicating if we want to get just the permissions directly assigned for the group and path, - /// or fall back to the group's default permissions when nothing is directly assigned + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// EntityPermission GetPermissionsForPath(IUserGroup group, string path, bool fallbackToDefaultPermissions = false); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 841a51456e..52e3e82106 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -865,21 +865,18 @@ namespace Umbraco.Core.Services /// /// Group to retrieve permissions for /// - /// Flag indicating if we want to get just the permissions directly assigned for the group and path, - /// or fall back to the group's default permissions when nothing is directly assigned + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of private IEnumerable GetPermissions(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) { - if (group == null) throw new ArgumentNullException("group"); - if (nodeIds.Length == 0) - return Enumerable.Empty(); - + if (group == null) throw new ArgumentNullException("group"); + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); - return GetPermissionsInternal(repository, group, fallbackToDefaultPermissions, nodeIds); + return repository.GetPermissionsForEntities(group, fallbackToDefaultPermissions, nodeIds); } } @@ -888,8 +885,7 @@ namespace Umbraco.Core.Services /// /// Group to retrieve permissions for /// - /// Flag indicating if we want to get just the permissions directly assigned for the group and path, - /// or fall back to the group's default permissions when nothing is directly assigned + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of @@ -899,28 +895,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); - return GetPermissionsInternal(repository, group.ToReadOnlyGroup(), fallbackToDefaultPermissions, nodeIds); + return repository.GetPermissionsForEntities(group.ToReadOnlyGroup(), fallbackToDefaultPermissions, nodeIds); } - } - - private static IEnumerable GetPermissionsInternal(IUserGroupRepository repository, IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (group == null) throw new ArgumentNullException("group"); - - var explicitPermissions = repository.GetPermissionsForEntities(group.Id, nodeIds); - var result = new List(explicitPermissions); - - // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults - if (fallbackToDefaultPermissions) - { - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToArray(); - if (missingIds.Length > 0) - { - result.AddRange(missingIds - .Select(i => new EntityPermission(i, group.Permissions.ToArray(), isDefaultPermissions: true))); - } - } - return result; } /// @@ -952,49 +928,18 @@ namespace Umbraco.Core.Services /// Path to check permissions for public EntityPermissionSet GetPermissionsForPath(IUser user, string path) { - var nodeIds = GetIdsFromPath(path); + var nodeIds = path.GetIdsFromPathReversed(); if (nodeIds.Length == 0) return EntityPermissionSet.Empty(); - var permissionsByGroup = GetPermissionsForGroupsAndPath(user.Groups, nodeIds); - - // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? - if (permissionsByGroup.Count == 0) - return EntityPermissionSet.Empty(); - - var entityId = nodeIds[0]; - - var groupPermissions = new List(); - foreach (var entityPermission in permissionsByGroup) - { - var groupId = entityPermission.Key; - foreach (var assignedPermission in entityPermission.Value.AssignedPermissions) - { - groupPermissions.Add(new EntityPermissionSet.UserGroupPermission(groupId, assignedPermission)); - } - } - - var permissionSet = new EntityPermissionSet(entityId, groupPermissions); - return permissionSet; - } - - /// - /// Retrieves the permissions assigned to each group for a given path - /// - /// List of groups associated with the user - /// Path to check permissions for - /// A dictionary of group ids and their associated node permissions - private IDictionary GetPermissionsForGroupsAndPath(IEnumerable groups, int[] pathIds) - { - return groups - .Select(g => new - { - group = g.Id, - permissions = GetPermissionsForPath(g, pathIds, fallbackToDefaultPermissions: true) - }) - .ToDictionary(x => x.group, x => x.permissions); - } + //collect all permissions structures for all nodes for all groups belonging to the user + var groupPermissions = user.Groups + .Select(g => GetPermissionsForPath(g, nodeIds, fallbackToDefaultPermissions: true)) + .ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } /// /// Gets the permissions for the provided group and path @@ -1002,13 +947,12 @@ namespace Umbraco.Core.Services /// Group to check permissions for /// Path to check permissions for /// - /// Flag indicating if we want to get just the permissions directly assigned for the group and path, - /// or fall back to the group's default permissions when nothing is directly assigned + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// String indicating permissions for provided user and path public EntityPermission GetPermissionsForPath(IUserGroup group, string path, bool fallbackToDefaultPermissions = false) { - var nodeIds = GetIdsFromPath(path); + var nodeIds = path.GetIdsFromPathReversed(); return GetPermissionsForPath(group.ToReadOnlyGroup(), nodeIds, fallbackToDefaultPermissions); } @@ -1016,17 +960,91 @@ namespace Umbraco.Core.Services { //get permissions for all nodes in the path var permissions = GetPermissions(group, fallbackToDefaultPermissions, pathIds); - return GetPermissionsForPath(permissions, pathIds, fallbackToDefaultPermissions); - } - + return GetPermissionsForPathForGroup(permissions, pathIds, fallbackToDefaultPermissions); + } + /// - /// Returns the permissions for the path ids + /// This performs the calculations for inherited nodes based on this http://issues.umbraco.org/issue/U4-10075#comment=67-40085 /// - /// - /// Must be ordered deepest to shallowest (right to left) - /// + /// + /// /// - private static EntityPermission GetPermissionsForPath( + internal static EntityPermissionSet CalculatePermissionsForPathForUser( + EntityPermission[] groupPermissions, + int[] pathIds) + { + // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? + if (groupPermissions.Length == 0 || pathIds.Length == 0) + return EntityPermissionSet.Empty(); + + //The actual entity id being looked at (deepest part of the path) + var entityId = pathIds[0]; + + var resultPermissions = new List(); + + //create a grouped by dictionary of another grouped by dictionary + var permissionsByGroup = groupPermissions + .GroupBy(x => x.UserGroupId) + .ToDictionary( + x => x.Key, + x => x.GroupBy(a => a.EntityId).ToDictionary(a => a.Key, a => a.ToArray())); + + //iterate through each group + foreach (var byGroup in permissionsByGroup) + { + var added = false; + + //iterate deepest to shallowest + foreach (var pathId in pathIds) + { + EntityPermission[] permissionsForNodeAndGroup; + if (byGroup.Value.TryGetValue(pathId, out permissionsForNodeAndGroup) == false) + continue; + + //In theory there will only be one EntityPermission in this group + // but there's nothing stopping the logic of this method + // from having more so we deal with it here + foreach (var entityPermission in permissionsForNodeAndGroup) + { + if (entityPermission.IsDefaultPermissions == false) + { + //explicit permision found so we'll append it and move on, of course if there was two explicit permissions + //found for this group the ones after this one wouldn't matter but considering there should only be one per + //group anyways, that is fine. + resultPermissions.Add(entityPermission); + added = true; + break; + } + } + + //if the permission has been added for this group and this branch then we can exit this loop + if (added) + break; + } + + if (added == false && byGroup.Value.Count > 0) + { + //if there was no explicit permissions assigned in this branch for this group, then we will + //add the group's default permissions + resultPermissions.Add(byGroup.Value[entityId][0]); + } + + } + + var permissionSet = new EntityPermissionSet(entityId, resultPermissions); + return permissionSet; + } + + /// + /// Returns the resulting permission set for a group for the path based on all permissions provided for the branch + /// + /// The collective set of permissions provided to calculate the resulting permissions set for the path + /// Must be ordered deepest to shallowest (right to left) + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// + internal static EntityPermission GetPermissionsForPathForGroup( IEnumerable pathPermissions, int[] pathIds, bool fallbackToDefaultPermissions = false) @@ -1051,24 +1069,8 @@ namespace Umbraco.Core.Services return null; return permissionsByEntityId[pathIds[0]]; - } - - /// - /// Convert a path to node ids in the order from right to left (deepest to shallowest) - /// - /// - /// - private int[] GetIdsFromPath(string path) - { - var nodeIds = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.TryConvertTo()) - .Where(x => x.Success) - .Select(x => x.Result) - .Reverse() - .ToArray(); - return nodeIds; } - + /// /// Checks in a set of permissions associated with a user for those related to a given nodeId /// diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index dba7f2cfee..0780fe799c 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -41,6 +41,22 @@ namespace Umbraco.Core ToCSharpEscapeChars[escape[0]] = escape[1]; } + /// + /// Convert a path to node ids in the order from right to left (deepest to shallowest) + /// + /// + /// + internal static int[] GetIdsFromPathReversed(this string path) + { + var nodeIds = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.TryConvertTo()) + .Where(x => x.Success) + .Select(x => x.Result) + .Reverse() + .ToArray(); + return nodeIds; + } + /// /// Removes new lines and tabs /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8a635b966c..7269dcd9f6 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -345,6 +345,7 @@ + @@ -456,7 +457,6 @@ - @@ -588,6 +588,7 @@ + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 2b9e0b1ed3..f969488055 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -533,48 +533,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(dateValue, persistedTextpage.GetValue(dateTimePropertyAlias)); Assert.AreEqual(persistedTextpage.GetValue(dateTimePropertyAlias), textpage.GetValue(dateTimePropertyAlias)); } - } - - [Test] - public void Ensures_Permissions_Are_Set_If_Parent_Entity_Permissions_Exist() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - - using (var repository = CreateUserGroupRepository(unitOfWork)) - { - var userGroup = MockedUserGroup.CreateUserGroup("1"); - repository.AddOrUpdate(userGroup); - unitOfWork.Commit(); - } - - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - contentType.AllowedContentTypes = new List - { - new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) - }; - var parentPage = MockedContent.CreateSimpleContent(contentType); - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(parentPage); - unitOfWork.Commit(); - - // Act - repository.AssignEntityPermission(parentPage, 'A', new[] { 1 }); - var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); - repository.AddOrUpdate(childPage); - unitOfWork.Commit(); - - // Assert - var permissions = repository.GetPermissionsForEntity(childPage.Id); - Assert.AreEqual(1, permissions.Count()); - Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); - } - - } + } [Test] public void Can_Perform_Add_On_ContentRepository() diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 93202af102..43f049adf4 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1459,7 +1459,7 @@ namespace Umbraco.Tests.Services } [Test] - public void Ensures_Permissions_Are_Set_On_Copied_Entity_To_Parent_Without_Permissions() + public void Ensures_Permissions_Are_Retained_For_Copied_Descendants_With_Explicit_Permissions() { // Arrange var userGroup = MockedUserGroup.CreateUserGroup("1"); @@ -1472,27 +1472,28 @@ namespace Umbraco.Tests.Services }; ServiceContext.ContentTypeService.Save(contentType); - var parentPage = MockedContent.CreateSimpleContent(contentType); - ServiceContext.ContentService.Save(parentPage); - ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { 1 }); + var parentPage = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage); var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); - ServiceContext.ContentService.Save(childPage); - - //Ok, now copy, what should happen is the childPage will not have any permissions assigned since it's new parent - //doesn't have any assigned + ServiceContext.ContentService.Save(childPage); + //assign explicit permissions to the child + ServiceContext.ContentService.AssignContentPermission(childPage, 'A', new[] { userGroup.Id }); + + //Ok, now copy, what should happen is the childPage will retain it's own permissions var parentPage2 = MockedContent.CreateSimpleContent(contentType); ServiceContext.ContentService.Save(parentPage2); var copy = ServiceContext.ContentService.Copy(childPage, parentPage2.Id, false, true); - //Re-get the permissions, since there was none assigned to the new parent, the child shouldn't have any directly assigned - var copyPermissions = ServiceContext.ContentService.GetPermissionsForEntity(copy); - Assert.AreEqual(0, copyPermissions.Count()); + //get the permissions and verify + var permissions = ServiceContext.UserService.GetPermissionsForPath(userGroup, copy.Path, fallbackToDefaultPermissions: true); + Assert.AreEqual(1, permissions.AssignedPermissions.Length); + Assert.AreEqual("A", permissions.AssignedPermissions[0]); } [Test] - public void Ensures_Permissions_Are_Set_On_Copied_Entity_To_Parent_With_Permissions() + public void Ensures_Permissions_Are_Inherited_For_Copied_Descendants() { // Arrange var userGroup = MockedUserGroup.CreateUserGroup("1"); @@ -1507,88 +1508,8 @@ namespace Umbraco.Tests.Services var parentPage = MockedContent.CreateSimpleContent(contentType); ServiceContext.ContentService.Save(parentPage); - ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { 1 }); + ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { userGroup.Id }); - var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); - ServiceContext.ContentService.Save(childPage); - - //Ok, now copy, what should happen is the childPage will have it's new parent permissions copied over - var parentPage2 = MockedContent.CreateSimpleContent(contentType); - ServiceContext.ContentService.Save(parentPage2); - ServiceContext.ContentService.AssignContentPermission(parentPage2, 'B', new[] { 1 }); - - var copy = ServiceContext.ContentService.Copy(childPage, parentPage2.Id, false, true); - - //Re-get the permissions, since there was none assigned to the new parent, the child shouldn't have any directly assigned - var copyPermissions = ServiceContext.ContentService.GetPermissionsForEntity(copy); - Assert.AreEqual(1, copyPermissions.Count()); - Assert.AreEqual("B", copyPermissions.Single().AssignedPermissions.First()); - } - - [Test] - public void Ensures_Permissions_Are_Set_On_Copied_Descendants_To_Parent_With_Permissions() - { - // Arrange - var userGroup = MockedUserGroup.CreateUserGroup("1"); - ServiceContext.UserService.Save(userGroup); - - var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - contentType.AllowedContentTypes = new List - { - new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) - }; - ServiceContext.ContentTypeService.Save(contentType); - - var parentPage = MockedContent.CreateSimpleContent(contentType); - ServiceContext.ContentService.Save(parentPage); - ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { 1 }); - - var childPage1 = MockedContent.CreateSimpleContent(contentType, "child1", parentPage); - ServiceContext.ContentService.Save(childPage1); - var childPage2 = MockedContent.CreateSimpleContent(contentType, "child2", childPage1); - ServiceContext.ContentService.Save(childPage2); - var childPage3 = MockedContent.CreateSimpleContent(contentType, "child3", childPage2); - ServiceContext.ContentService.Save(childPage3); - - //Ok, now copy, what should happen is the childPage will have it's new parent permissions copied over - var parentPage2 = MockedContent.CreateSimpleContent(contentType); - ServiceContext.ContentService.Save(parentPage2); - ServiceContext.ContentService.AssignContentPermission(parentPage2, 'B', new[] { 1 }); - - var copy = ServiceContext.ContentService.Copy(childPage1, parentPage2.Id, false, true); - - //Re-get the permissions, since there was none assigned to the new parent, the child shouldn't have any directly assigned - var copyPermissions = ServiceContext.ContentService.GetPermissionsForEntity(copy); - Assert.AreEqual(1, copyPermissions.Count()); - Assert.AreEqual("B", copyPermissions.Single().AssignedPermissions.First()); - - var descendants = ServiceContext.ContentService.GetDescendants(copy).ToArray(); - Assert.AreEqual(2, descendants.Length); - - foreach (var descendant in descendants) - { - var permissions = ServiceContext.ContentService.GetPermissionsForEntity(descendant); - Assert.AreEqual(1, permissions.Count()); - Assert.AreEqual("B", permissions.Single().AssignedPermissions.First()); - } - } - - [Test] - public void Ensures_Permissions_Are_Set_On_Descendants_When_Permissions_Added() - { - // Arrange - var userGroup = MockedUserGroup.CreateUserGroup("1"); - ServiceContext.UserService.Save(userGroup); - - var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - contentType.AllowedContentTypes = new List - { - new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) - }; - ServiceContext.ContentTypeService.Save(contentType); - - var parentPage = MockedContent.CreateSimpleContent(contentType); - ServiceContext.ContentService.Save(parentPage); var childPage1 = MockedContent.CreateSimpleContent(contentType, "child1", parentPage); ServiceContext.ContentService.Save(childPage1); var childPage2 = MockedContent.CreateSimpleContent(contentType, "child2", childPage1); @@ -1596,37 +1517,33 @@ namespace Umbraco.Tests.Services var childPage3 = MockedContent.CreateSimpleContent(contentType, "child3", childPage2); ServiceContext.ContentService.Save(childPage3); - //ensure there are no permissions on any node - - var permissions = ServiceContext.ContentService.GetPermissionsForEntity(parentPage); - Assert.AreEqual(0, permissions.Count()); - + //Verify that the children have the inherited permissions var descendants = ServiceContext.ContentService.GetDescendants(parentPage).ToArray(); Assert.AreEqual(3, descendants.Length); foreach (var descendant in descendants) { - permissions = ServiceContext.ContentService.GetPermissionsForEntity(descendant); - Assert.AreEqual(0, permissions.Count()); + var permissions = ServiceContext.UserService.GetPermissionsForPath(userGroup, descendant.Path, fallbackToDefaultPermissions:true); + Assert.AreEqual(1, permissions.AssignedPermissions.Length); + Assert.AreEqual("A", permissions.AssignedPermissions[0]); } - - //Ok, now assign permissions to the parent, all descenents should get these right? - ServiceContext.ContentService.AssignContentPermission(parentPage, 'A', new[] { 1 }); - - //re-test - permissions = ServiceContext.ContentService.GetPermissionsForEntity(parentPage); - Assert.AreEqual(1, permissions.Count()); - Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); + //create a new parent with a new permission structure + var parentPage2 = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(parentPage2); + ServiceContext.ContentService.AssignContentPermission(parentPage2, 'B', new[] { userGroup.Id }); - descendants = ServiceContext.ContentService.GetDescendants(parentPage).ToArray(); + //Now copy, what should happen is the child pages will now have permissions inherited from the new parent + var copy = ServiceContext.ContentService.Copy(childPage1, parentPage2.Id, false, true); + + descendants = ServiceContext.ContentService.GetDescendants(parentPage2).ToArray(); Assert.AreEqual(3, descendants.Length); foreach (var descendant in descendants) { - permissions = ServiceContext.ContentService.GetPermissionsForEntity(descendant); - Assert.AreEqual(1, permissions.Count()); - Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); + var permissions = ServiceContext.UserService.GetPermissionsForPath(userGroup, descendant.Path, fallbackToDefaultPermissions: true); + Assert.AreEqual(1, permissions.AssignedPermissions.Length); + Assert.AreEqual("B", permissions.AssignedPermissions[0]); } } diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index d672229e6b..ac58f9f734 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -10,6 +11,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Tests.TestHelpers; using Umbraco.Core; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Services; namespace Umbraco.Tests.Services { @@ -151,7 +153,7 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); // Act - var permissions = userService.GetPermissions(userGroup, true, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id) + var permissions = userService.GetPermissions(userGroup, true, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id) .ToArray(); //assert @@ -161,6 +163,132 @@ namespace Umbraco.Tests.Services Assert.AreEqual(17,permissions[2].AssignedPermissions.Length); } + [Test] + + [Test] + public void Calculate_Permissions_For_User_For_Path_1() + { + //see: http://issues.umbraco.org/issue/U4-10075#comment=67-40085 + // for an overview of what this is testing + + const string path = "-1,1,2,3,4"; + var pathIds = path.GetIdsFromPathReversed(); + + const int groupA = 7; + const int groupB = 8; + const int groupC = 9; + + var userGroups = new Dictionary + { + {groupA, new[] {"S", "D", "F"}}, + {groupB, new[] {"S", "D", "G", "K"}}, + {groupC, new[] {"F", "G"}} + }; + + var permissions = new [] + { + new EntityPermission(groupA, 1, userGroups[groupA], isDefaultPermissions:true), + new EntityPermission(groupA, 2, userGroups[groupA], isDefaultPermissions:true), + new EntityPermission(groupA, 3, userGroups[groupA], isDefaultPermissions:true), + new EntityPermission(groupA, 4, userGroups[groupA], isDefaultPermissions:true), + + new EntityPermission(groupB, 1, userGroups[groupB], isDefaultPermissions:true), + new EntityPermission(groupB, 2, new []{"F", "R"}, isDefaultPermissions:false), + new EntityPermission(groupB, 3, userGroups[groupB], isDefaultPermissions:true), + new EntityPermission(groupB, 4, userGroups[groupB], isDefaultPermissions:true), + + new EntityPermission(groupC, 1, userGroups[groupC], isDefaultPermissions:true), + new EntityPermission(groupC, 2, userGroups[groupC], isDefaultPermissions:true), + new EntityPermission(groupC, 3, new []{"Q", "Z"}, isDefaultPermissions:false), + new EntityPermission(groupC, 4, userGroups[groupC], isDefaultPermissions:true), + }; + + //Permissions for Id 4 + var result = UserService.CalculatePermissionsForPathForUser(permissions, pathIds); + Assert.AreEqual(4, result.EntityId); + var allPermissions = result.GetAllPermissions().ToArray(); + Assert.AreEqual(6, allPermissions.Length, string.Join(",", allPermissions)); + Assert.IsTrue(allPermissions.ContainsAll(new[] { "S", "D", "F", "R", "Q", "Z" })); + + //Permissions for Id 3 + result = UserService.CalculatePermissionsForPathForUser(permissions, pathIds.Skip(1).ToArray()); + Assert.AreEqual(3, result.EntityId); + allPermissions = result.GetAllPermissions().ToArray(); + Assert.AreEqual(6, allPermissions.Length, string.Join(",", allPermissions)); + Assert.IsTrue(allPermissions.ContainsAll(new[] { "S", "D", "F", "R", "Q", "Z" })); + + //Permissions for Id 2 + result = UserService.CalculatePermissionsForPathForUser(permissions, pathIds.Skip(2).ToArray()); + Assert.AreEqual(2, result.EntityId); + allPermissions = result.GetAllPermissions().ToArray(); + Assert.AreEqual(5, allPermissions.Length, string.Join(",", allPermissions)); + Assert.IsTrue(allPermissions.ContainsAll(new[] { "S", "D", "F", "G", "R" })); + + //Permissions for Id 1 + result = UserService.CalculatePermissionsForPathForUser(permissions, pathIds.Skip(3).ToArray()); + Assert.AreEqual(1, result.EntityId); + allPermissions = result.GetAllPermissions().ToArray(); + Assert.AreEqual(5, allPermissions.Length, string.Join(",", allPermissions)); + Assert.IsTrue(allPermissions.ContainsAll(new[] { "S", "D", "F", "G", "K" })); + + } + + [Test] + public void Determine_Deepest_Explicit_Permissions_For_Group_For_Path_1() + { + var path = "-1,1,2,3"; + var pathIds = path.GetIdsFromPathReversed(); + var defaults = new[] {"A", "B"}; + var permissions = new List + { + new EntityPermission(9876, 1, defaults, isDefaultPermissions:true), + new EntityPermission(9876, 2, new []{"B","C", "D"}, isDefaultPermissions:false), + new EntityPermission(9876, 3, defaults, isDefaultPermissions:true) + }; + var result = UserService.GetPermissionsForPathForGroup(permissions, pathIds, fallbackToDefaultPermissions: true); + Assert.AreEqual(3, result.AssignedPermissions.Length); + Assert.IsFalse(result.IsDefaultPermissions); + Assert.IsTrue(result.AssignedPermissions.ContainsAll(new[] { "B", "C", "D" })); + Assert.AreEqual(2, result.EntityId); + Assert.AreEqual(9876, result.UserGroupId); + } + + [Test] + public void Determine_Deepest_Explicit_Permissions_For_Group_For_Path_2() + { + var path = "-1,1,2,3"; + var pathIds = path.GetIdsFromPathReversed(); + var defaults = new[] { "A", "B", "C" }; + var permissions = new List + { + new EntityPermission(9876, 1, defaults, isDefaultPermissions:true), + new EntityPermission(9876, 2, defaults, isDefaultPermissions:true), + new EntityPermission(9876, 3, defaults, isDefaultPermissions:true) + }; + var result = UserService.GetPermissionsForPathForGroup(permissions, pathIds, fallbackToDefaultPermissions:false); + Assert.IsNull(result); + } + + [Test] + public void Determine_Deepest_Explicit_Permissions_For_Group_For_Path_3() + { + var path = "-1,1,2,3"; + var pathIds = path.GetIdsFromPathReversed(); + var defaults = new[] { "A", "B" }; + var permissions = new List + { + new EntityPermission(9876, 1, defaults, isDefaultPermissions:true), + new EntityPermission(9876, 2, defaults, isDefaultPermissions:true), + new EntityPermission(9876, 3, defaults, isDefaultPermissions:true) + }; + var result = UserService.GetPermissionsForPathForGroup(permissions, pathIds, fallbackToDefaultPermissions: true); + Assert.AreEqual(2, result.AssignedPermissions.Length); + Assert.IsTrue(result.IsDefaultPermissions); + Assert.IsTrue(result.AssignedPermissions.ContainsAll(defaults)); + Assert.AreEqual(3, result.EntityId); + Assert.AreEqual(9876, result.UserGroupId); + } + [Test] public void Get_User_Implicit_Permissions() { diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs index f4480ca636..bc1f5dfe77 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs @@ -50,7 +50,8 @@ namespace Umbraco.Tests.Web.Controllers var contentService = contentServiceMock.Object; var userServiceMock = new Mock(); var permissions = new List(); - userServiceMock.Setup(x => x.GetPermissions(user, 1234)).Returns(permissions); + var permissionSet = new EntityPermissionSet(1234, permissions); + userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234,5678")).Returns(permissionSet); var userService = userServiceMock.Object; //act/assert @@ -73,7 +74,8 @@ namespace Umbraco.Tests.Web.Controllers var contentService = contentServiceMock.Object; var userServiceMock = new Mock(); var permissions = new List(); - userServiceMock.Setup(x => x.GetPermissions(user, 1234)).Returns(permissions); + var permissionSet = new EntityPermissionSet(1234, permissions); + userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234")).Returns(permissionSet); var userService = userServiceMock.Object; //act @@ -100,9 +102,10 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(1234, new string[]{ "A", "B", "C" }) + new EntityPermission(9876, 1234, new string[]{ "A", "B", "C" }) }; - userServiceMock.Setup(x => x.GetPermissions(user, 1234)).Returns(permissions); + var permissionSet = new EntityPermissionSet(1234, permissions); + userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234,5678")).Returns(permissionSet); var userService = userServiceMock.Object; //act @@ -129,9 +132,10 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(1234, new string[]{ "A", "F", "C" }) + new EntityPermission(9876, 1234, new string[]{ "A", "F", "C" }) }; - userServiceMock.Setup(x => x.GetPermissions(user, 1234)).Returns(permissions); + var permissionSet = new EntityPermissionSet(1234, permissions); + userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234,5678")).Returns(permissionSet); var userService = userServiceMock.Object; //act @@ -217,9 +221,10 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(1234, new string[]{ "A" }) + new EntityPermission(9876, 1234, new string[]{ "A" }) }; - userServiceMock.Setup(x => x.GetPermissions(user, -1)).Returns(permissions); + var permissionSet = new EntityPermissionSet(1234, permissions); + userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1")).Returns(permissionSet); var userService = userServiceMock.Object; //act @@ -241,9 +246,10 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(1234, new string[]{ "A" }) + new EntityPermission(9876, 1234, new string[]{ "A" }) }; - userServiceMock.Setup(x => x.GetPermissions(user, -1)).Returns(permissions); + var permissionSet = new EntityPermissionSet(1234, permissions); + userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1")).Returns(permissionSet); var userService = userServiceMock.Object; //act @@ -264,10 +270,12 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var permissions = new List - { - new EntityPermission(1234, new string[]{ "A" }) - }; - userServiceMock.Setup(x => x.GetPermissions(user, -20)).Returns(permissions); + { + new EntityPermission(9876, 1234, new string[]{ "A" }) + }; + var permissionSet = new EntityPermissionSet(-20, permissions); + + userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-20")).Returns(permissionSet); var userService = userServiceMock.Object; //act @@ -289,9 +297,10 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var permissions = new List { - new EntityPermission(1234, new string[]{ "A" }) + new EntityPermission(9876, 1234, new string[]{ "A" }) }; - userServiceMock.Setup(x => x.GetPermissions(user, -20)).Returns(permissions); + var permissionSet = new EntityPermissionSet(1234, permissions); + userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-20")).Returns(permissionSet); var userService = userServiceMock.Object; //act diff --git a/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs b/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs index 8e84dcf030..49ce356324 100644 --- a/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs @@ -112,10 +112,10 @@ namespace Umbraco.Tests.Web.Controllers //we're only assigning 3 nodes browse permissions so that is what we expect as a result var permissions = new List { - new EntityPermission(1, new string[]{ "F" }), - new EntityPermission(2, new string[]{ "F" }), - new EntityPermission(3, new string[]{ "F" }), - new EntityPermission(4, new string[]{ "A" }) + new EntityPermission(9876, 1, new string[]{ "F" }), + new EntityPermission(9876, 2, new string[]{ "F" }), + new EntityPermission(9876, 3, new string[]{ "F" }), + new EntityPermission(9876, 4, new string[]{ "A" }) }; userServiceMock.Setup(x => x.GetPermissions(user, ids)).Returns(permissions); var userService = userServiceMock.Object; diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 4b65eb1b3a..8e628204d8 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -599,10 +599,13 @@ namespace Umbraco.Web.Cache #region User/permissions event handlers - static void CacheRefresherEventHandler_AssignedPermissions(PermissionRepository sender, SaveEventArgs e) + static void CacheRefresherEventHandler_AssignedPermissions(PermissionRepository sender, SaveEventArgs e) { var groupIds = e.SavedEntities.Select(x => x.UserGroupId).Distinct(); - groupIds.ForEach(x => DistributedCache.Instance.RefreshUserGroupPermissionsCache(x)); + foreach (var groupId in groupIds) + { + DistributedCache.Instance.RefreshUserGroupPermissionsCache(groupId); + } } static void PermissionDeleted(UserGroupPermission sender, DeleteEventArgs e) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 8fd2d8f221..f935673317 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1076,17 +1076,21 @@ namespace Umbraco.Web.Editors return false; } - if (permissionsToCheck == null || permissionsToCheck.Any() == false) + if (permissionsToCheck == null || permissionsToCheck.Length == 0) { return true; } - - var permission = userService.GetPermissions(user, nodeId).FirstOrDefault(); + + //get the implicit/inherited permissions for the user for this path, + //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + var path = contentItem != null ? contentItem.Path : nodeId.ToString(); + var permission = userService.GetPermissionsForPath(user, path); var allowed = true; foreach (var p in permissionsToCheck) { - if (permission == null || permission.AssignedPermissions.Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) { allowed = false; } diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 0f92a4427f..dcac5a0caf 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -572,7 +572,7 @@ namespace umbraco.BusinessLogic var userService = ApplicationContext.Current.Services.UserService; return string.Join("", - userService.GetPermissionsForPath(UserEntity, path).PermissionsSet.SelectMany(x => x.Permission)); + userService.GetPermissionsForPath(UserEntity, path).GetAllPermissions()); } /// From 87dcdc596e906926f3062a98521b0c2e34b26ef6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Jul 2017 01:56:22 +1000 Subject: [PATCH 380/714] Gets last unit tests written for returning all permissions for either user or group when not specifying Ids --- .../Models/Membership/ContentPermissionSet.cs | 2 +- .../Models/Membership/EntityPermission.cs | 41 ++--- .../Membership/EntityPermissionCollection.cs | 34 +++++ .../Models/Membership/EntityPermissionSet.cs | 15 +- .../Repositories/ContentRepository.cs | 2 +- .../Interfaces/IContentRepository.cs | 2 +- .../Interfaces/IUserGroupRepository.cs | 4 +- .../Repositories/PermissionRepository.cs | 93 +++++------- .../Repositories/UserGroupRepository.cs | 13 +- src/Umbraco.Core/Services/ContentService.cs | 8 +- .../Services/ContentServiceExtensions.cs | 2 +- src/Umbraco.Core/Services/IContentService.cs | 2 +- src/Umbraco.Core/Services/IUserService.cs | 10 +- src/Umbraco.Core/Services/UserService.cs | 42 ++--- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Services/UserServiceTests.cs | 143 ++++++++++++++---- .../Controllers/ContentControllerUnitTests.cs | 16 +- ...terAllowedOutgoingContentAttributeTests.cs | 2 +- 18 files changed, 262 insertions(+), 170 deletions(-) create mode 100644 src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs diff --git a/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs index 1345be2c8c..ca0a910c05 100644 --- a/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Models.Membership { private readonly IContent _content; - public ContentPermissionSet(IContent content, IEnumerable permissionsSet) + public ContentPermissionSet(IContent content, EntityPermissionCollection permissionsSet) : base(content.Id, permissionsSet) { _content = content; diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs index c6d9e8350d..8f609c0e13 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs @@ -1,11 +1,11 @@ -using System.Linq; +using System; namespace Umbraco.Core.Models.Membership { /// /// Represents an entity permission (defined on the user group and derived to retrieve permissions for a given user) /// - public class EntityPermission + public class EntityPermission : IEquatable { public EntityPermission(int groupId, int entityId, string[] assignedPermissions) { @@ -37,25 +37,30 @@ namespace Umbraco.Core.Models.Membership /// /// This will be the case when looking up entity permissions and falling back to the default permissions /// - public bool IsDefaultPermissions { get; private set; } + public bool IsDefaultPermissions { get; private set; } - /// - /// Adds additional permissions to an existing instance of - /// ensuring that only ones that aren't already assigned are added - /// - /// - public void AddAdditionalPermissions(string[] additionalPermissions) - { - //TODO: Fix the performance of this, we need to use things like HashSet and equality checkers, we are iterating too much + public bool Equals(EntityPermission other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return EntityId == other.EntityId && UserGroupId == other.UserGroupId; + } - var newPermissions = AssignedPermissions.ToList(); - newPermissions.AddRange(additionalPermissions); - AssignedPermissions = newPermissions - .Distinct() - .ToArray(); - } - + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((EntityPermission) obj); + } + public override int GetHashCode() + { + unchecked + { + return (EntityId * 397) ^ UserGroupId; + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs new file mode 100644 index 0000000000..5fca079cfc --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models.Membership +{ + /// + /// A of + /// + public class EntityPermissionCollection : HashSet + { + public EntityPermissionCollection() + { + } + + public EntityPermissionCollection(IEnumerable collection) : base(collection) + { + } + + /// + /// Returns the aggregate permissions in the permission set + /// + /// + /// + /// This value is only calculated once + /// + public IEnumerable GetAllPermissions() + { + return _aggregatePermissions ?? (_aggregatePermissions = + this.SelectMany(x => x.AssignedPermissions).Distinct().ToArray()); + } + + private string[] _aggregatePermissions; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs index dd9190ae85..0f35fec990 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs @@ -14,10 +14,10 @@ namespace Umbraco.Core.Models.Membership /// public static EntityPermissionSet Empty() { - return new EntityPermissionSet(-1, new EntityPermission[0]); + return new EntityPermissionSet(-1, new EntityPermissionCollection()); } - public EntityPermissionSet(int entityId, IEnumerable permissionsSet) + public EntityPermissionSet(int entityId, EntityPermissionCollection permissionsSet) { EntityId = entityId; PermissionsSet = permissionsSet; @@ -31,10 +31,11 @@ namespace Umbraco.Core.Models.Membership /// /// The key/value pairs of user group id & single permission /// - public IEnumerable PermissionsSet { get; private set; } + public EntityPermissionCollection PermissionsSet { get; private set; } + /// - /// Returns the aggregte permissions in the permission set + /// Returns the aggregate permissions in the permission set /// /// /// @@ -42,15 +43,11 @@ namespace Umbraco.Core.Models.Membership /// public IEnumerable GetAllPermissions() { - return _calculatedPermissions ?? (_calculatedPermissions = - PermissionsSet.SelectMany(x => x.AssignedPermissions).Distinct().ToArray()); + return PermissionsSet.GetAllPermissions(); } - private string[] _calculatedPermissions; - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 2c3f126c6e..addd0bc925 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -847,7 +847,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", /// /// /// - public IEnumerable GetPermissionsForEntity(int entityId) + public EntityPermissionCollection GetPermissionsForEntity(int entityId) { return _permissionRepository.GetPermissionsForEntity(entityId); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 6f643b3e0b..c44c8ac1dc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -61,7 +61,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - IEnumerable GetPermissionsForEntity(int entityId); + EntityPermissionCollection GetPermissionsForEntity(int entityId); ///// ///// Gets the implicit/inherited list of permissions for the content item diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs index b018bd3c08..ec5fe79065 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Id of group /// Array of entity Ids, if empty will return permissions for the group for all entities - IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds); + EntityPermissionCollection GetPermissionsForEntities(int groupId, params int[] entityIds); /// /// Gets explicilt and default permissions (if requested) permissions for the group for specified entities @@ -38,7 +38,7 @@ namespace Umbraco.Core.Persistence.Repositories /// The group /// If true will include the group's default permissions if no permissions are explicitly assigned /// Array of entity Ids, if empty will return permissions for the group for all entities - IEnumerable GetPermissionsForEntities(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); + EntityPermissionCollection GetPermissionsForEntities(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); /// /// Replaces the same permission set for a single group to any number of entities diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index 4cad3be7e7..a22b5c5437 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -42,62 +42,43 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - public IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds) - //var whereCriteria = GetPermissionsForEntitiesCriteria(groupId, entityIds); - var result = new List(); - //iterate in groups of 2000 since we don't want to exceed the max SQL param count - foreach (var groupOfIds in entityIds.InGroupsOf(2000)) - { - var ids = groupOfIds; + public EntityPermissionCollection GetPermissionsForEntities(int groupId, params int[] entityIds) + { + var result = new EntityPermissionCollection(); - var sql = new Sql(); + if (entityIds.Length == 0) + { + var sql = new Sql(); sql.Select("*") .From(SqlSyntax) - .Where(dto => dto.UserGroupId == groupId && ids.Contains(dto.NodeId), SqlSyntax); + .Where(dto => dto.UserGroupId == groupId, SqlSyntax); var permissions = UnitOfWork.Database.Fetch(sql); - result.AddRange(ConvertToPermissionList(permissions)); - } - return result; - - - //var entityIdKey = GetEntityIdKey(entityIds); - - //return _runtimeCache.GetCacheItem>( - // string.Format("{0}{1}{2}", - // CacheKeys.UserGroupPermissionsCacheKey, - // groupId, - // entityIdKey), - // () => - // { - // var whereCriteria = GetPermissionsForEntitiesCriteria(groupId, entityIds); - // var sql = new Sql(); - // sql.Select("*") - // .From() - // .Where(whereCriteria); - // var result = UnitOfWork.Database.Fetch(sql); - // return ConvertToPermissionList(result); - // }, - // GetCacheTimeout(), - // priority: GetCachePriority()); - } - - //private static string GetEntityIdKey(int[] entityIds) - //{ - // return string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); - //} - - //private static TimeSpan GetCacheTimeout() - //{ - // //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will only have this exist in cache for 20 minutes, - // // then it will refresh from the database. - // return new TimeSpan(0, 20, 0); - //} - - //private static CacheItemPriority GetCachePriority() - //{ - // //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will make this priority below average - // return CacheItemPriority.BelowNormal; - //} + foreach (var permission in ConvertToPermissionList(permissions)) + { + result.Add(permission); + } + } + else + { + //iterate in groups of 2000 since we don't want to exceed the max SQL param count + foreach (var groupOfIds in entityIds.InGroupsOf(2000)) + { + var ids = groupOfIds; + + var sql = new Sql(); + sql.Select("*") + .From(SqlSyntax) + .Where(dto => dto.UserGroupId == groupId && ids.Contains(dto.NodeId), SqlSyntax); + var permissions = UnitOfWork.Database.Fetch(sql); + foreach (var permission in ConvertToPermissionList(permissions)) + { + result.Add(permission); + } + } + } + + return result; + } /// /// Returns permissions directly assigned to the content items for all user groups @@ -121,7 +102,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - public IEnumerable GetPermissionsForEntity(int entityId) + public EntityPermissionCollection GetPermissionsForEntity(int entityId) { var sql = new Sql(); sql.Select("*") @@ -343,16 +324,16 @@ namespace Umbraco.Core.Persistence.Repositories ReplaceEntityPermissions(entity); } - private static IEnumerable ConvertToPermissionList(IEnumerable result) + private static EntityPermissionCollection ConvertToPermissionList(IEnumerable result) { - var permissions = new List(); + var permissions = new EntityPermissionCollection(); var nodePermissions = result.GroupBy(x => x.NodeId); foreach (var np in nodePermissions) { var userGroupPermissions = np.GroupBy(x => x.UserGroupId); foreach (var permission in userGroupPermissions) { - var perms = permission.Select(x => x.Permission).ToArray(); + var perms = permission.Select(x => x.Permission).Distinct().ToArray(); permissions.Add(new EntityPermission(permission.Key, np.Key, perms)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index bd2b8390cd..619d896fde 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -97,7 +97,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Id of group /// Array of entity Ids, if empty will return permissions for the group for all entities - public IEnumerable GetPermissionsForEntities(int groupId, params int[] entityIds) + public EntityPermissionCollection GetPermissionsForEntities(int groupId, params int[] entityIds) { return _permissionRepository.GetPermissionsForEntities(groupId, entityIds); } @@ -108,12 +108,12 @@ namespace Umbraco.Core.Persistence.Repositories /// The group /// If true will include the group's default permissions if no permissions are explicitly assigned /// Array of entity Ids, if empty will return permissions for the group for all entities - public IEnumerable GetPermissionsForEntities(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) + public EntityPermissionCollection GetPermissionsForEntities(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) { if (group == null) throw new ArgumentNullException("group"); var explicitPermissions = GetPermissionsForEntities(group.Id, nodeIds); - var result = new List(explicitPermissions); + var result = new EntityPermissionCollection(explicitPermissions); // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults if (fallbackToDefaultPermissions) @@ -121,8 +121,11 @@ namespace Umbraco.Core.Persistence.Repositories var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToArray(); if (missingIds.Length > 0) { - result.AddRange(missingIds - .Select(i => new EntityPermission(group.Id, i, group.Permissions.ToArray(), isDefaultPermissions: true))); + foreach (var permission in missingIds + .Select(i => new EntityPermission(group.Id, i, group.Permissions.ToArray(), isDefaultPermissions: true))) + { + result.Add(permission); + } } } return result; diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index b7649ea382..491e03a065 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -134,7 +134,7 @@ namespace Umbraco.Core.Services /// /// /// - public IEnumerable GetPermissionsForEntity(IContent content) + public EntityPermissionCollection GetPermissionsForEntity(IContent content) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { @@ -1673,14 +1673,14 @@ namespace Umbraco.Core.Services copy.WriterId = userId; //get the current permissions, if there are any explicit ones they need to be copied - var currentPermissions = GetPermissionsForEntity(content) - .Where(x => x.IsDefaultPermissions == false).ToArray(); + var currentPermissions = GetPermissionsForEntity(content); + currentPermissions.RemoveWhere(p => p.IsDefaultPermissions); repository.AddOrUpdate(copy); repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); //add permissions - if (currentPermissions.Length > 0) + if (currentPermissions.Count > 0) { var permissionSet = new ContentPermissionSet(copy, currentPermissions); repository.AddOrUpdatePermissions(permissionSet); diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index c6dc0274b6..b3380cf1bf 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Services /// public static void RemoveContentPermissions(this IContentService contentService, int contentId) { - contentService.ReplaceContentPermissions(new EntityPermissionSet(contentId, Enumerable.Empty())); + contentService.ReplaceContentPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection())); } /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 251bf169ad..b048046ac0 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -155,7 +155,7 @@ namespace Umbraco.Core.Services /// /// /// - IEnumerable GetPermissionsForEntity(IContent content); + EntityPermissionCollection GetPermissionsForEntity(IContent content); bool SendToPublication(IContent content, int userId = 0); diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index c5b282cd66..5f1d16d6bc 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -83,8 +83,8 @@ namespace Umbraco.Core.Services /// /// This is useful when an entire section is removed from config /// Alias of the section to remove - void DeleteSectionFromAllUserGroups(string sectionAlias); - + void DeleteSectionFromAllUserGroups(string sectionAlias); + /// /// Get explicitly assigned permissions for a user and optional node ids /// @@ -95,8 +95,8 @@ namespace Umbraco.Core.Services /// /// This will return the default permissions for the user's groups for nodes that don't have explicitly defined permissions /// - IEnumerable GetPermissions(IUser user, params int[] nodeIds); - + EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds); + /// /// Get explicitly assigned permissions for a group and optional node Ids /// @@ -106,7 +106,7 @@ namespace Umbraco.Core.Services /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - IEnumerable GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); + EntityPermissionCollection GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); /// /// Gets the implicit/inherited permissions for the user for the given path diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 52e3e82106..149b37cbec 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Logging; +using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -845,15 +846,17 @@ namespace Umbraco.Core.Services /// User to retrieve permissions for /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - public IEnumerable GetPermissions(IUser user, params int[] nodeIds) - { - var result = new List(); + public EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds) + { + //TODO: we don't need to run this query for each group assigned, we can do this in one query + + var result = new EntityPermissionCollection(); foreach (var group in user.Groups) { foreach (var permission in GetPermissions(group, true, nodeIds)) { - AddOrAmendPermissionList(result, permission); + result.Add(permission); } } @@ -889,7 +892,7 @@ namespace Umbraco.Core.Services /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - public IEnumerable GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) + public EntityPermissionCollection GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) { if (group == null) throw new ArgumentNullException("group"); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) @@ -899,28 +902,6 @@ namespace Umbraco.Core.Services } } - /// - /// For an existing list of , takes a new and aggregates it. - /// If a permission for the entity associated with the new permission already exists, it's updated with those permissions to create a distinct, most permissive set. - /// If it doesn't, it's added to the list. - /// - /// List of already found permissions - /// New permission to aggregate - private static void AddOrAmendPermissionList(ICollection permissions, EntityPermission groupPermission) - { - //TODO: Fix the performance of this, we need to use things like HashSet and equality checkers, we are iterating too much - - var existingPermission = permissions.FirstOrDefault(x => x.EntityId == groupPermission.EntityId); - if (existingPermission != null) - { - existingPermission.AddAdditionalPermissions(groupPermission.AssignedPermissions); - } - else - { - permissions.Add(groupPermission); - } - } - /// /// Gets the implicit/inherited permissions for the user for the given path /// @@ -980,7 +961,7 @@ namespace Umbraco.Core.Services //The actual entity id being looked at (deepest part of the path) var entityId = pathIds[0]; - var resultPermissions = new List(); + var resultPermissions = new EntityPermissionCollection(); //create a grouped by dictionary of another grouped by dictionary var permissionsByGroup = groupPermissions @@ -1008,9 +989,8 @@ namespace Umbraco.Core.Services { if (entityPermission.IsDefaultPermissions == false) { - //explicit permision found so we'll append it and move on, of course if there was two explicit permissions - //found for this group the ones after this one wouldn't matter but considering there should only be one per - //group anyways, that is fine. + //explicit permision found so we'll append it and move on, the collection is a hashset anyways + //so only supports adding one element per groupid/contentid resultPermissions.Add(entityPermission); added = true; break; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7269dcd9f6..f9b1c7a752 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -346,6 +346,7 @@ + diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index ac58f9f734..610a6498c8 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -80,21 +80,21 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType) }; ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[1], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[1], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[2], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); // Act - var permissions = userService.GetPermissions(user, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); + var permissions = userService.GetPermissions(user, content[0].Id, content[1].Id, content[2].Id).ToArray(); //assert - Assert.AreEqual(3, permissions.Count()); - Assert.AreEqual(3, permissions.ElementAt(0).AssignedPermissions.Length); - Assert.AreEqual(2, permissions.ElementAt(1).AssignedPermissions.Length); - Assert.AreEqual(1, permissions.ElementAt(2).AssignedPermissions.Length); + Assert.AreEqual(3, permissions.Length); + Assert.AreEqual(3, permissions[0].AssignedPermissions.Length); + Assert.AreEqual(2, permissions[1].AssignedPermissions.Length); + Assert.AreEqual(1, permissions[2].AssignedPermissions.Length); } [Test] @@ -121,13 +121,13 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.AssignContentPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); // Act - var permissions = userService.GetPermissions(userGroup, false, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id); + var permissions = userService.GetPermissions(userGroup, false, content[0].Id, content[1].Id, content[2].Id).ToArray(); //assert - Assert.AreEqual(3, permissions.Count()); - Assert.AreEqual(3, permissions.ElementAt(0).AssignedPermissions.Length); - Assert.AreEqual(2, permissions.ElementAt(1).AssignedPermissions.Length); - Assert.AreEqual(1, permissions.ElementAt(2).AssignedPermissions.Length); + Assert.AreEqual(3, permissions.Length); + Assert.AreEqual(3, permissions[0].AssignedPermissions.Length); + Assert.AreEqual(2, permissions[1].AssignedPermissions.Length); + Assert.AreEqual(1, permissions[2].AssignedPermissions.Length); } [Test] @@ -146,14 +146,14 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType) }; ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.AssignContentPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[1], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[1], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); // Act - var permissions = userService.GetPermissions(userGroup, true, content.ElementAt(0).Id, content.ElementAt(1).Id, content.ElementAt(2).Id) + var permissions = userService.GetPermissions(userGroup, true, content[0].Id, content[1].Id, content[2].Id) .ToArray(); //assert @@ -164,9 +164,100 @@ namespace Umbraco.Tests.Services } [Test] + public void Get_All_User_Permissions_For_All_Nodes() + { + // Arrange + var userService = ServiceContext.UserService; + var userGroup1 = CreateTestUserGroup(); + var userGroup2 = CreateTestUserGroup("test2", "Test 2"); + var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io"); + + user.AddGroup(userGroup1); + user.AddGroup(userGroup2); + userService.Save(user); + + var contentType = MockedContentTypes.CreateSimpleContentType(); + ServiceContext.ContentTypeService.Save(contentType); + var content = new[] + { + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType) + }; + ServiceContext.ContentService.Save(content); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup1.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup1.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup2.Id }); + ServiceContext.ContentService.AssignContentPermission(content[1], ActionBrowse.Instance.Letter, new int[] { userGroup1.Id }); + ServiceContext.ContentService.AssignContentPermission(content[1], ActionDelete.Instance.Letter, new int[] { userGroup2.Id }); + ServiceContext.ContentService.AssignContentPermission(content[2], ActionDelete.Instance.Letter, new int[] { userGroup1.Id }); + + // Act + //we don't pass in any nodes so it will return all of them + var result = userService.GetPermissions(user).ToArray(); + var permissions = result + .GroupBy(x => x.EntityId) + .ToDictionary(x => x.Key, x => x.GroupBy(a => a.UserGroupId).ToDictionary(a => a.Key, a => a.ToArray())); + + //assert + Assert.AreEqual(3, permissions.Count); + Assert.IsTrue(permissions.ContainsKey(content[0].Id)); + Assert.IsTrue(permissions[content[0].Id].ContainsKey(userGroup1.Id)); + Assert.IsTrue(permissions[content[0].Id].ContainsKey(userGroup2.Id)); + Assert.AreEqual(2, permissions[content[0].Id][userGroup1.Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.AreEqual(1, permissions[content[0].Id][userGroup2.Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.IsTrue(permissions.ContainsKey(content[1].Id)); + Assert.IsTrue(permissions[content[1].Id].ContainsKey(userGroup1.Id)); + Assert.IsTrue(permissions[content[1].Id].ContainsKey(userGroup2.Id)); + Assert.AreEqual(1, permissions[content[1].Id][userGroup1.Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.AreEqual(1, permissions[content[1].Id][userGroup2.Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.IsTrue(permissions.ContainsKey(content[2].Id)); + Assert.IsTrue(permissions[content[2].Id].ContainsKey(userGroup1.Id)); + Assert.IsFalse(permissions[content[2].Id].ContainsKey(userGroup2.Id)); + Assert.AreEqual(1, permissions[content[2].Id][userGroup1.Id].SelectMany(x => x.AssignedPermissions).Count()); + } [Test] - public void Calculate_Permissions_For_User_For_Path_1() + public void Get_All_User_Group_Permissions_For_All_Nodes() + { + // Arrange + var userService = ServiceContext.UserService; + var userGroup = CreateTestUserGroup(); + + var contentType = MockedContentTypes.CreateSimpleContentType(); + ServiceContext.ContentTypeService.Save(contentType); + var content = new[] + { + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType), + MockedContent.CreateSimpleContent(contentType) + }; + ServiceContext.ContentService.Save(content); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[1], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[1], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.AssignContentPermission(content[2], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + + // Act + //we don't pass in any nodes so it will return all of them + var permissions = userService.GetPermissions(userGroup, true) + .GroupBy(x => x.EntityId) + .ToDictionary(x => x.Key, x => x); + + //assert + Assert.AreEqual(3, permissions.Count); + Assert.IsTrue(permissions.ContainsKey(content[0].Id)); + Assert.AreEqual(3, permissions[content[0].Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.IsTrue(permissions.ContainsKey(content[1].Id)); + Assert.AreEqual(2, permissions[content[1].Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.IsTrue(permissions.ContainsKey(content[2].Id)); + Assert.AreEqual(1, permissions[content[2].Id].SelectMany(x => x.AssignedPermissions).Count()); + } + + [Test] + public void Calculate_Permissions_For_User_For_Path() { //see: http://issues.umbraco.org/issue/U4-10075#comment=67-40085 // for an overview of what this is testing @@ -857,12 +948,12 @@ namespace Umbraco.Tests.Services return user; } - private UserGroup CreateTestUserGroup() + private UserGroup CreateTestUserGroup(string alias = "testGroup", string name = "Test Group") { var userGroup = new UserGroup { - Alias = "testGroup", - Name = "Test Group", + Alias = alias, + Name = name, Permissions = "ABCDEFGHIJ1234567".ToCharArray().Select(x => x.ToString()) }; diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs index bc1f5dfe77..23177cf419 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs @@ -49,7 +49,7 @@ namespace Umbraco.Tests.Web.Controllers contentServiceMock.Setup(x => x.GetById(0)).Returns(content); var contentService = contentServiceMock.Object; var userServiceMock = new Mock(); - var permissions = new List(); + var permissions = new EntityPermissionCollection(); var permissionSet = new EntityPermissionSet(1234, permissions); userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234,5678")).Returns(permissionSet); var userService = userServiceMock.Object; @@ -73,7 +73,7 @@ namespace Umbraco.Tests.Web.Controllers contentServiceMock.Setup(x => x.GetById(1234)).Returns(content); var contentService = contentServiceMock.Object; var userServiceMock = new Mock(); - var permissions = new List(); + var permissions = new EntityPermissionCollection(); var permissionSet = new EntityPermissionSet(1234, permissions); userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234")).Returns(permissionSet); var userService = userServiceMock.Object; @@ -100,7 +100,7 @@ namespace Umbraco.Tests.Web.Controllers contentServiceMock.Setup(x => x.GetById(1234)).Returns(content); var contentService = contentServiceMock.Object; var userServiceMock = new Mock(); - var permissions = new List + var permissions = new EntityPermissionCollection { new EntityPermission(9876, 1234, new string[]{ "A", "B", "C" }) }; @@ -130,7 +130,7 @@ namespace Umbraco.Tests.Web.Controllers contentServiceMock.Setup(x => x.GetById(1234)).Returns(content); var contentService = contentServiceMock.Object; var userServiceMock = new Mock(); - var permissions = new List + var permissions = new EntityPermissionCollection { new EntityPermission(9876, 1234, new string[]{ "A", "F", "C" }) }; @@ -219,7 +219,7 @@ namespace Umbraco.Tests.Web.Controllers var user = userMock.Object; var userServiceMock = new Mock(); - var permissions = new List + var permissions = new EntityPermissionCollection { new EntityPermission(9876, 1234, new string[]{ "A" }) }; @@ -244,7 +244,7 @@ namespace Umbraco.Tests.Web.Controllers var user = userMock.Object; var userServiceMock = new Mock(); - var permissions = new List + var permissions = new EntityPermissionCollection { new EntityPermission(9876, 1234, new string[]{ "A" }) }; @@ -269,7 +269,7 @@ namespace Umbraco.Tests.Web.Controllers var user = userMock.Object; var userServiceMock = new Mock(); - var permissions = new List + var permissions = new EntityPermissionCollection { new EntityPermission(9876, 1234, new string[]{ "A" }) }; @@ -295,7 +295,7 @@ namespace Umbraco.Tests.Web.Controllers var user = userMock.Object; var userServiceMock = new Mock(); - var permissions = new List + var permissions = new EntityPermissionCollection { new EntityPermission(9876, 1234, new string[]{ "A" }) }; diff --git a/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs b/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs index 49ce356324..5afce04a1a 100644 --- a/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs @@ -110,7 +110,7 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); //we're only assigning 3 nodes browse permissions so that is what we expect as a result - var permissions = new List + var permissions = new EntityPermissionCollection { new EntityPermission(9876, 1, new string[]{ "F" }), new EntityPermission(9876, 2, new string[]{ "F" }), From 76f9e2c22a0348180a3462b19b1643a374f95756 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Jul 2017 10:43:42 +1000 Subject: [PATCH 381/714] Updates user permissions lookup to do one query instead of one query per user group --- .../Interfaces/IUserGroupRepository.cs | 8 +-- .../Repositories/PermissionRepository.cs | 54 +++++++++++-------- .../Repositories/UserGroupRepository.cs | 32 ++++++----- src/Umbraco.Core/Services/UserService.cs | 18 +++---- 4 files changed, 63 insertions(+), 49 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs index ec5fe79065..dd6188e31c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserGroupRepository.cs @@ -28,17 +28,17 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Gets explicilty defined permissions for the group for specified entities /// - /// Id of group + /// /// Array of entity Ids, if empty will return permissions for the group for all entities - EntityPermissionCollection GetPermissionsForEntities(int groupId, params int[] entityIds); + EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds); /// /// Gets explicilt and default permissions (if requested) permissions for the group for specified entities /// - /// The group + /// /// If true will include the group's default permissions if no permissions are explicitly assigned /// Array of entity Ids, if empty will return permissions for the group for all entities - EntityPermissionCollection GetPermissionsForEntities(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); + EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds); /// /// Replaces the same permission set for a single group to any number of entities diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index a22b5c5437..7dc751040d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -39,43 +39,53 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Returns explicitly defined permissions for a user group for any number of nodes /// - /// + /// + /// The group ids to lookup permissions for + /// /// /// - public EntityPermissionCollection GetPermissionsForEntities(int groupId, params int[] entityIds) + /// + /// This method will not support passing in more than 2000 group Ids + /// + public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) { var result = new EntityPermissionCollection(); - if (entityIds.Length == 0) + foreach (var groupOfGroupIds in groupIds.InGroupsOf(2000)) { - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .Where(dto => dto.UserGroupId == groupId, SqlSyntax); - var permissions = UnitOfWork.Database.Fetch(sql); - foreach (var permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } - } - else - { - //iterate in groups of 2000 since we don't want to exceed the max SQL param count - foreach (var groupOfIds in entityIds.InGroupsOf(2000)) - { - var ids = groupOfIds; + //copy local + var localIds = groupOfGroupIds.ToArray(); - var sql = new Sql(); + if (entityIds.Length == 0) + { + var sql = new Sql(); sql.Select("*") .From(SqlSyntax) - .Where(dto => dto.UserGroupId == groupId && ids.Contains(dto.NodeId), SqlSyntax); + .Where(dto => localIds.Contains(dto.UserGroupId), SqlSyntax); var permissions = UnitOfWork.Database.Fetch(sql); foreach (var permission in ConvertToPermissionList(permissions)) { result.Add(permission); } + } + else + { + //iterate in groups of 2000 since we don't want to exceed the max SQL param count + foreach (var groupOfEntityIds in entityIds.InGroupsOf(2000)) + { + var ids = groupOfEntityIds; + var sql = new Sql(); + sql.Select("*") + .From(SqlSyntax) + .Where(dto => localIds.Contains(dto.UserGroupId) && ids.Contains(dto.NodeId), SqlSyntax); + var permissions = UnitOfWork.Database.Fetch(sql); + foreach (var permission in ConvertToPermissionList(permissions)) + { + result.Add(permission); + } + } } - } + } return result; } diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index 619d896fde..dec6148bb8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -91,42 +91,46 @@ namespace Umbraco.Core.Persistence.Repositories _userGroupWithUsersRepository.AddOrUpdate(new UserGroupWithUsers(userGroup, userIds)); } - + /// /// Gets explicilty defined permissions for the group for specified entities /// - /// Id of group + /// /// Array of entity Ids, if empty will return permissions for the group for all entities - public EntityPermissionCollection GetPermissionsForEntities(int groupId, params int[] entityIds) + public EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds) { - return _permissionRepository.GetPermissionsForEntities(groupId, entityIds); + return _permissionRepository.GetPermissionsForEntities(groupIds, entityIds); } /// /// Gets explicilt and default permissions (if requested) permissions for the group for specified entities /// - /// The group + /// /// If true will include the group's default permissions if no permissions are explicitly assigned /// Array of entity Ids, if empty will return permissions for the group for all entities - public EntityPermissionCollection GetPermissionsForEntities(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) + public EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) { - if (group == null) throw new ArgumentNullException("group"); + if (groups == null) throw new ArgumentNullException("groups"); - var explicitPermissions = GetPermissionsForEntities(group.Id, nodeIds); + var groupIds = groups.Select(x => x.Id).ToArray(); + var explicitPermissions = GetPermissions(groupIds, nodeIds); var result = new EntityPermissionCollection(explicitPermissions); // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults if (fallbackToDefaultPermissions) { - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToArray(); - if (missingIds.Length > 0) + foreach (var group in groups) { - foreach (var permission in missingIds - .Select(i => new EntityPermission(group.Id, i, group.Permissions.ToArray(), isDefaultPermissions: true))) + var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToArray(); + if (missingIds.Length > 0) { - result.Add(permission); + foreach (var permission in missingIds + .Select(i => new EntityPermission(group.Id, i, group.Permissions.ToArray(), isDefaultPermissions: true))) + { + result.Add(permission); + } } - } + } } return result; } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 149b37cbec..0d31e9d281 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -847,19 +847,19 @@ namespace Umbraco.Core.Services /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of public EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds) - { - //TODO: we don't need to run this query for each group assigned, we can do this in one query - + { var result = new EntityPermissionCollection(); - foreach (var group in user.Groups) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - foreach (var permission in GetPermissions(group, true, nodeIds)) + var repository = RepositoryFactory.CreateUserGroupRepository(uow); + + foreach (var permission in repository.GetPermissions(user.Groups.ToArray(), true, nodeIds)) { result.Add(permission); } - } - + } + return result; } @@ -879,7 +879,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); - return repository.GetPermissionsForEntities(group, fallbackToDefaultPermissions, nodeIds); + return repository.GetPermissions(new[] { group }, fallbackToDefaultPermissions, nodeIds); } } @@ -898,7 +898,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); - return repository.GetPermissionsForEntities(group.ToReadOnlyGroup(), fallbackToDefaultPermissions, nodeIds); + return repository.GetPermissions(new[] { group.ToReadOnlyGroup() }, fallbackToDefaultPermissions, nodeIds); } } From c472463f37d7b39aefb25ef360c1c9639917d44a Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Jul 2017 11:02:34 +1000 Subject: [PATCH 382/714] Removes all user permissions cache refreshing and events and obsoletes assocated clsses --- src/Umbraco.Core/Cache/CacheKeys.cs | 4 +- src/Umbraco.Core/Models/Content.cs | 12 +-- .../Repositories/PermissionRepository.cs | 22 +---- .../Cache/CacheRefresherEventHandler.cs | 92 +------------------ .../Cache/DistributedCacheExtensions.cs | 18 ---- .../Cache/UserGroupCacheRefresher.cs | 18 +--- .../UserGroupPermissionsCacheRefresher.cs | 36 +------- 7 files changed, 18 insertions(+), 184 deletions(-) diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index f41bc92053..c9991ba45a 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -55,7 +55,9 @@ namespace Umbraco.Core.Cache [Obsolete("This is no longer used and will be removed from the codebase in the future")] [EditorBrowsable(EditorBrowsableState.Never)] public const string UserCacheKey = "UmbracoUser"; - + + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string UserGroupPermissionsCacheKey = "UmbracoUserGroupPermissions"; [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index cef06ec4f7..b3fbe4164b 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -87,7 +87,6 @@ namespace Umbraco.Core.Models public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ExpireDate); public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); public readonly PropertyInfo NodeNameSelector = ExpressionHelper.GetPropertyInfo(x => x.NodeName); - public readonly PropertyInfo PermissionsChangedSelector = ExpressionHelper.GetPropertyInfo(x => x.PermissionsChanged); } /// @@ -196,16 +195,7 @@ namespace Umbraco.Core.Models get { return _nodeName; } set { SetPropertyValueAndDetectChanges(value, ref _nodeName, Ps.Value.NodeNameSelector); } } - - /// - /// Used internally to track if permissions have been changed during the saving process for this entity - /// - [IgnoreDataMember] - internal bool PermissionsChanged - { - get { return _permissionsChanged; } - set { SetPropertyValueAndDetectChanges(value, ref _permissionsChanged, Ps.Value.PermissionsChangedSelector); } - } + /// /// Gets the ContentType used by this content object diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index 7dc751040d..7dc6996b57 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -159,9 +159,6 @@ namespace Umbraco.Core.Persistence.Repositories } UnitOfWork.Database.BulkInsertRecords(toInsert, SqlSyntax); - - //Raise the event - UnitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(toInsert), false)); } @@ -191,10 +188,7 @@ namespace Umbraco.Core.Persistence.Repositories }).ToArray(); UnitOfWork.Database.BulkInsertRecords(actions, SqlSyntax); - - //Raise the event - UnitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); - + } /// @@ -223,10 +217,7 @@ namespace Umbraco.Core.Persistence.Repositories }).ToArray(); UnitOfWork.Database.BulkInsertRecords(actions, SqlSyntax); - - //Raise the event - UnitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); - + } /// @@ -258,11 +249,7 @@ namespace Umbraco.Core.Persistence.Repositories } UnitOfWork.Database.BulkInsertRecords(toInsert, SqlSyntax); - - //Raise the event - UnitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(permissionSet.PermissionsSet, false)); - - + } @@ -350,7 +337,6 @@ namespace Umbraco.Core.Persistence.Repositories return permissions; } - - public static event TypedEventHandler, SaveEventArgs> AssignedPermissions; + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 8e628204d8..daf405a562 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -111,22 +111,8 @@ namespace Umbraco.Web.Cache Bind(() => MemberTypeService.Saved += MemberTypeService_Saved, () => MemberTypeService.Saved -= MemberTypeService_Saved); Bind(() => MemberTypeService.Deleted += MemberTypeService_Deleted, - () => MemberTypeService.Deleted -= MemberTypeService_Deleted); - - // bind to permission events - // we should wrap legacy permissions so we can get rid of this - // fixme - the method names here (PermissionNew...) are not supported - // by the event handling mechanism for scopes and deploy, and not sure - // how to fix with the generic repository - Bind(() => Permission.New += PermissionNew, - () => Permission.New -= PermissionNew); - Bind(() => Permission.Updated += PermissionUpdated, - () => Permission.Updated -= PermissionUpdated); - Bind(() => Permission.Deleted += PermissionDeleted, - () => Permission.Deleted -= PermissionDeleted); - Bind(() => PermissionRepository.AssignedPermissions += CacheRefresherEventHandler_AssignedPermissions, - () => PermissionRepository.AssignedPermissions -= CacheRefresherEventHandler_AssignedPermissions); - + () => MemberTypeService.Deleted -= MemberTypeService_Deleted); + // bind to template events Bind(() => FileService.SavedTemplate += FileService_SavedTemplate, () => FileService.SavedTemplate -= FileService_SavedTemplate); @@ -360,13 +346,6 @@ namespace Umbraco.Web.Cache /// static void ContentService_Copied(IContentService sender, CopyEventArgs e) { - //check if permissions have changed - var permissionsChanged = ((Content)e.Copy).WasPropertyDirty("PermissionsChanged"); - if (permissionsChanged) - { - DistributedCache.Instance.RefreshAllUserGroupPermissionsCache(); - } - //run the un-published cache refresher since copied content is not published DistributedCache.Instance.RefreshUnpublishedPageCache(e.Copy); } @@ -388,33 +367,10 @@ namespace Umbraco.Web.Cache /// /// /// When an entity is saved we need to notify other servers about the change in order for the Examine indexes to - /// stay up-to-date for unpublished content. - /// - /// When an entity is created new permissions may be assigned to it based on it's parent, if that is the - /// case then we need to clear all user group permissions cache. + /// stay up-to-date for unpublished content. /// static void ContentService_Saved(IContentService sender, SaveEventArgs e) { - var clearUserGroupPermissions = false; - e.SavedEntities.ForEach(x => - { - //check if it is new - if (x.IsNewEntity()) - { - //check if permissions have changed - var permissionsChanged = ((Content)x).WasPropertyDirty("PermissionsChanged"); - if (permissionsChanged) - { - clearUserGroupPermissions = true; - } - } - }); - - if (clearUserGroupPermissions) - { - DistributedCache.Instance.RefreshAllUserGroupPermissionsCache(); - } - //filter out the entities that have only been saved (not newly published) since // newly published ones will be synced with the published page cache refresher var unpublished = e.SavedEntities.Where(x => x.JustPublished() == false); @@ -598,30 +554,7 @@ namespace Umbraco.Web.Cache #endregion #region User/permissions event handlers - - static void CacheRefresherEventHandler_AssignedPermissions(PermissionRepository sender, SaveEventArgs e) - { - var groupIds = e.SavedEntities.Select(x => x.UserGroupId).Distinct(); - foreach (var groupId in groupIds) - { - DistributedCache.Instance.RefreshUserGroupPermissionsCache(groupId); - } - } - - static void PermissionDeleted(UserGroupPermission sender, DeleteEventArgs e) - { - InvalidateCacheForPermissionsChange(sender); - } - - static void PermissionUpdated(UserGroupPermission sender, SaveEventArgs e) - { - InvalidateCacheForPermissionsChange(sender); - } - - static void PermissionNew(UserGroupPermission sender, NewEventArgs e) - { - InvalidateCacheForPermissionsChange(sender); - } + static void UserService_SavedUser(IUserService sender, SaveEventArgs e) { @@ -642,22 +575,7 @@ namespace Umbraco.Web.Cache { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserGroupCache(x.Id)); } - - private static void InvalidateCacheForPermissionsChange(UserGroupPermission sender) - { - if (sender.UserGroup != null) - { - DistributedCache.Instance.RefreshUserGroupPermissionsCache(sender.UserGroup.Id); - } - else if (sender.UserId > -1) - { - DistributedCache.Instance.RefreshUserGroupPermissionsCache(sender.UserId); - } - else if (sender.NodeIds.Any()) - { - DistributedCache.Instance.RefreshAllUserGroupPermissionsCache(); - } - } + #endregion diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index f8e8b412c5..397410dbd5 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -79,24 +79,6 @@ namespace Umbraco.Web.Cache #endregion - #region User group permissions cache - - public static void RemoveUserGroupPermissionsCache(this DistributedCache dc, int groupId) - { - dc.Remove(DistributedCache.UserGroupPermissionsCacheRefresherGuid, groupId); - } - - public static void RefreshUserGroupPermissionsCache(this DistributedCache dc, int groupId) - { - dc.Refresh(DistributedCache.UserGroupPermissionsCacheRefresherGuid, groupId); - } - - public static void RefreshAllUserGroupPermissionsCache(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.UserGroupPermissionsCacheRefresherGuid); - } - - #endregion #region Template cache diff --git a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs index e6447d1f45..84496b61fd 100644 --- a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs @@ -34,10 +34,6 @@ namespace Umbraco.Web.Cache { userGroupCache.Result.ClearCacheByKeySearch(UserGroupRepository.GetByAliasCacheKeyPrefix); } - if (UserGroupPermissionsCache) - { - UserGroupPermissionsCache.Result.ClearCacheByKeySearch(CacheKeys.UserGroupPermissionsCacheKey); - } base.RefreshAll(); } @@ -56,20 +52,8 @@ namespace Umbraco.Web.Cache userGroupCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); userGroupCache.Result.ClearCacheByKeySearch(UserGroupRepository.GetByAliasCacheKeyPrefix); } - - if (UserGroupPermissionsCache) - { - //TODO: Is this good enough for all users attached to this? - var keyStartsWith = string.Format("{0}{1}", CacheKeys.UserGroupPermissionsCacheKey, id); - UserGroupPermissionsCache.Result.ClearCacheByKeySearch(keyStartsWith); - } - + base.Remove(id); } - - private Attempt UserGroupPermissionsCache - { - get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs b/src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs index 8ea1b26f47..405da0b306 100644 --- a/src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserGroupPermissionsCacheRefresher.cs @@ -1,17 +1,13 @@ using System; +using System.ComponentModel; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models.Membership; namespace Umbraco.Web.Cache { - /// - /// Used only to invalidate the user permissions cache - /// - /// - /// The UserGroupCacheRefresher will also clear a groups's permissions cache, this refresher is for invalidating only permissions - /// for user groups/content, not the groups themselves. - /// + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class UserGroupPermissionsCacheRefresher : CacheRefresherBase { protected override UserGroupPermissionsCacheRefresher Instance @@ -29,30 +25,6 @@ namespace Umbraco.Web.Cache { get { return "User group permissions cache refresher"; } } - - public override void RefreshAll() - { - if (UserGroupPermissionsCache) - UserGroupPermissionsCache.Result.ClearCacheByKeySearch(CacheKeys.UserGroupPermissionsCacheKey); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - if (UserGroupPermissionsCache) - UserGroupPermissionsCache.Result.ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.UserGroupPermissionsCacheKey, id)); - base.Remove(id); - } - - private Attempt UserGroupPermissionsCache - { - get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } - } + } } \ No newline at end of file From 69a6a80ffd99d614e38d7c65788bdf0f7d7dc2d1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Jul 2017 14:16:13 +1000 Subject: [PATCH 383/714] Updates logic and test for getting all node permissions for a user that have explicit permissions defined --- .../Repositories/UserGroupRepository.cs | 20 ++++++---- src/Umbraco.Core/Services/IUserService.cs | 4 +- src/Umbraco.Core/Services/UserService.cs | 11 +---- .../Services/UserServiceTests.cs | 40 +++++++++++++++---- src/umbraco.cms/businesslogic/Permission.cs | 6 +-- 5 files changed, 53 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index dec6148bb8..137cc0885b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -119,16 +119,22 @@ namespace Umbraco.Core.Persistence.Repositories // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults if (fallbackToDefaultPermissions) { + //if no node ids are passed in, then we need to determine the node ids for the explicit permissions set + nodeIds = nodeIds.Length == 0 + ? explicitPermissions.Select(x => x.EntityId).Distinct().ToArray() + : nodeIds; + + //if there are still no nodeids we can just exit + if (nodeIds.Length == 0) + return result; + foreach (var group in groups) { - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)).ToArray(); - if (missingIds.Length > 0) + foreach (var nodeId in nodeIds) { - foreach (var permission in missingIds - .Select(i => new EntityPermission(group.Id, i, group.Permissions.ToArray(), isDefaultPermissions: true))) - { - result.Add(permission); - } + var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions.ToArray(), isDefaultPermissions: true); + //Since this is a hashset, this will not add anything that already exists by group/node combination + result.Add(defaultPermission); } } } diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 5f1d16d6bc..5ae31f8c0d 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -90,10 +90,10 @@ namespace Umbraco.Core.Services /// /// If no permissions are found for a particular entity then the user's default permissions will be applied /// User to retrieve permissions for - /// Specifiying nothing will return all user permissions for all nodes + /// Specifiying nothing will return all user permissions for all nodes that have explicit permissions defined /// An enumerable list of /// - /// This will return the default permissions for the user's groups for nodes that don't have explicitly defined permissions + /// This will return the default permissions for the user's groups for node ids that don't have explicitly defined permissions /// EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 0d31e9d281..24ce233cc2 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -848,19 +848,12 @@ namespace Umbraco.Core.Services /// An enumerable list of public EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds) { - var result = new EntityPermissionCollection(); - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); - foreach (var permission in repository.GetPermissions(user.Groups.ToArray(), true, nodeIds)) - { - result.Add(permission); - } - } - - return result; + return repository.GetPermissions(user.Groups.ToArray(), true, nodeIds); + } } /// diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 610a6498c8..cdfff6ff0d 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -164,16 +164,20 @@ namespace Umbraco.Tests.Services } [Test] - public void Get_All_User_Permissions_For_All_Nodes() + public void Get_All_User_Permissions_For_All_Nodes_With_Explicit_Permission() { // Arrange var userService = ServiceContext.UserService; var userGroup1 = CreateTestUserGroup(); var userGroup2 = CreateTestUserGroup("test2", "Test 2"); - var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io"); + var userGroup3 = CreateTestUserGroup("test3", "Test 3"); + var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io"); + + var defaultPermissionCount = userGroup3.Permissions.Count(); user.AddGroup(userGroup1); user.AddGroup(userGroup2); + user.AddGroup(userGroup3); userService.Save(user); var contentType = MockedContentTypes.CreateSimpleContentType(); @@ -184,7 +188,8 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType), MockedContent.CreateSimpleContent(contentType) }; - ServiceContext.ContentService.Save(content); + ServiceContext.ContentService.Save(content); + //assign permissions - we aren't assigning anything explicit for group3 and nothing explicit for content[2] /w group2 ServiceContext.ContentService.AssignContentPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup1.Id }); ServiceContext.ContentService.AssignContentPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup1.Id }); ServiceContext.ContentService.AssignContentPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup2.Id }); @@ -199,22 +204,43 @@ namespace Umbraco.Tests.Services .GroupBy(x => x.EntityId) .ToDictionary(x => x.Key, x => x.GroupBy(a => a.UserGroupId).ToDictionary(a => a.Key, a => a.ToArray())); - //assert - Assert.AreEqual(3, permissions.Count); - Assert.IsTrue(permissions.ContainsKey(content[0].Id)); + //assert + + //there will be 3 since that is how many content items there are + Assert.AreEqual(3, permissions.Count); + + //test permissions contains content[0] + Assert.IsTrue(permissions.ContainsKey(content[0].Id)); + //test that this permissions set contains permissions for all groups Assert.IsTrue(permissions[content[0].Id].ContainsKey(userGroup1.Id)); Assert.IsTrue(permissions[content[0].Id].ContainsKey(userGroup2.Id)); + Assert.IsTrue(permissions[content[0].Id].ContainsKey(userGroup3.Id)); + //test that the correct number of permissions are returned for each group Assert.AreEqual(2, permissions[content[0].Id][userGroup1.Id].SelectMany(x => x.AssignedPermissions).Count()); Assert.AreEqual(1, permissions[content[0].Id][userGroup2.Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.AreEqual(defaultPermissionCount, permissions[content[0].Id][userGroup3.Id].SelectMany(x => x.AssignedPermissions).Count()); + + //test permissions contains content[1] Assert.IsTrue(permissions.ContainsKey(content[1].Id)); + //test that this permissions set contains permissions for all groups Assert.IsTrue(permissions[content[1].Id].ContainsKey(userGroup1.Id)); Assert.IsTrue(permissions[content[1].Id].ContainsKey(userGroup2.Id)); + Assert.IsTrue(permissions[content[1].Id].ContainsKey(userGroup3.Id)); + //test that the correct number of permissions are returned for each group Assert.AreEqual(1, permissions[content[1].Id][userGroup1.Id].SelectMany(x => x.AssignedPermissions).Count()); Assert.AreEqual(1, permissions[content[1].Id][userGroup2.Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.AreEqual(defaultPermissionCount, permissions[content[1].Id][userGroup3.Id].SelectMany(x => x.AssignedPermissions).Count()); + + //test permissions contains content[2] Assert.IsTrue(permissions.ContainsKey(content[2].Id)); + //test that this permissions set contains permissions for all groups Assert.IsTrue(permissions[content[2].Id].ContainsKey(userGroup1.Id)); - Assert.IsFalse(permissions[content[2].Id].ContainsKey(userGroup2.Id)); + Assert.IsTrue(permissions[content[2].Id].ContainsKey(userGroup2.Id)); + Assert.IsTrue(permissions[content[2].Id].ContainsKey(userGroup3.Id)); + //test that the correct number of permissions are returned for each group Assert.AreEqual(1, permissions[content[2].Id][userGroup1.Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.AreEqual(defaultPermissionCount, permissions[content[2].Id][userGroup2.Id].SelectMany(x => x.AssignedPermissions).Count()); + Assert.AreEqual(defaultPermissionCount, permissions[content[2].Id][userGroup3.Id].SelectMany(x => x.AssignedPermissions).Count()); } [Test] diff --git a/src/umbraco.cms/businesslogic/Permission.cs b/src/umbraco.cms/businesslogic/Permission.cs index 4bbe4604a4..335535c08d 100644 --- a/src/umbraco.cms/businesslogic/Permission.cs +++ b/src/umbraco.cms/businesslogic/Permission.cs @@ -9,15 +9,15 @@ using Umbraco.Core.Events; using umbraco.DataLayer; using umbraco.cms.businesslogic; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using DeleteEventArgs = umbraco.cms.businesslogic.DeleteEventArgs; namespace umbraco.BusinessLogic { - /// - /// Summary description for Permission. - /// + [Obsolete("This is no longer used and will be removed in future versions, use the IUserService to manage permissions for Users")] + [EditorBrowsable(EditorBrowsableState.Never)] public class Permission { From 6e892020024cbbbbeabbf556dcea55604fd7d645 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Jul 2017 14:32:43 +1000 Subject: [PATCH 384/714] Fixes usages of IUserService.GetPermission since before it was doing a FirstOrDefault() since that's just how the old APIs worked, now we need to get all permissions based on groups. Also updated the usages of ToString for char for the permissions to be culture invariant everywhere. --- src/Umbraco.Web/Editors/ContentController.cs | 6 +++--- .../Models/ContentEditing/ContentItemDisplay.cs | 2 +- .../Models/Mapping/ContentModelMapper.cs | 17 +++++++---------- .../FilterAllowedOutgoingContentAttribute.cs | 2 +- .../umbraco/Trees/BaseContentTree.cs | 2 +- .../umbraco/dialogs/moveOrCopy.aspx.cs | 4 ++-- .../umbraco/dialogs/notifications.aspx.cs | 3 ++- .../umbraco/templateControls/Item.cs | 3 ++- src/umbraco.cms/Actions/Action.cs | 3 ++- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index f935673317..b624041992 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -242,7 +242,7 @@ namespace Umbraco.Web.Editors //set a custom path since the tree that renders this has the content type id as the parent content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); - content.AllowedActions = new[] {'A'}; + content.AllowedActions = new[] {"A"}; var excludeProps = new[] {"_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"}; var propsTab = content.Tabs.Last(); @@ -440,8 +440,8 @@ namespace Umbraco.Web.Editors [HttpGet] public bool HasPermission(string permissionToCheck, int nodeId) { - var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).FirstOrDefault(); - if (p != null && p.AssignedPermissions.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) + var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); + if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) { return true; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index 2615c8774d..c416f30a48 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -56,7 +56,7 @@ namespace Umbraco.Web.Models.ContentEditing /// Each char represents a button which we can then map on the front-end to the correct actions /// [DataMember(Name = "allowedActions")] - public IEnumerable AllowedActions { get; set; } + public IEnumerable AllowedActions { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index a33921b7b9..3b55def135 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -135,7 +135,7 @@ namespace Umbraco.Web.Models.Mapping Label = localizedText.Localize("content/releaseDate"), Value = display.ReleaseDate.HasValue ? display.ReleaseDate.Value.ToIsoString() : null, //Not editible for people without publish permission (U4-287) - View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, + View = display.AllowedActions.Contains(ActionPublish.Instance.Letter.ToString(CultureInfo.InvariantCulture)) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, Config = new Dictionary { {"offsetTime", "1"} @@ -148,7 +148,7 @@ namespace Umbraco.Web.Models.Mapping Label = localizedText.Localize("content/unpublishDate"), Value = display.ExpireDate.HasValue ? display.ExpireDate.Value.ToIsoString() : null, //Not editible for people without publish permission (U4-287) - View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, + View = display.AllowedActions.Contains(ActionPublish.Instance.Letter.ToString(CultureInfo.InvariantCulture)) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, Config = new Dictionary { {"offsetTime", "1"} @@ -213,10 +213,9 @@ namespace Umbraco.Web.Models.Mapping } /// - //TODO: This is horribly inneficient /// Creates the list of action buttons allowed for this user - Publish, Send to publish, save, unpublish returned as the button's 'letter' /// - private class ActionButtonsResolver : ValueResolver> + private class ActionButtonsResolver : ValueResolver> { private readonly Lazy _userService; @@ -225,12 +224,12 @@ namespace Umbraco.Web.Models.Mapping _userService = userService; } - protected override IEnumerable ResolveCore(IContent source) + protected override IEnumerable ResolveCore(IContent source) { if (UmbracoContext.Current == null) { //cannot check permissions without a context - return Enumerable.Empty(); + return Enumerable.Empty(); } var svc = _userService.Value; @@ -242,11 +241,9 @@ namespace Umbraco.Web.Models.Mapping // Here we need to do a special check since this could be new content, in which case we need to get the permissions // from the parent, not the existing one otherwise permissions would be coming from the root since Id is 0. source.HasIdentity ? source.Id : source.ParentId) - .FirstOrDefault(); + .GetAllPermissions(); - return permissions == null - ? Enumerable.Empty() - : permissions.AssignedPermissions.Where(x => x.Length == 1).Select(x => x.ToUpperInvariant()[0]); + return permissions; } } } diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs index ffd6bf362e..6091392321 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -74,7 +74,7 @@ namespace Umbraco.Web.WebApi.Filters var nodePermission = permissions.Where(x => x.EntityId == Convert.ToInt32(item.Id)).ToArray(); //if there are no permissions for this id then we need to check what the user's default // permissions are. - if (nodePermission.Any() == false) + if (nodePermission.Length == 0) { //var defaultP = user.DefaultPermissions diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseContentTree.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseContentTree.cs index 1eacc072bd..e448d9653a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseContentTree.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseContentTree.cs @@ -360,7 +360,7 @@ function openContent(id) { } else if (!this.IsDialog || (this.DialogMode == TreeDialogModes.id)) { - if (CurrentUser.GetPermissions(dd.Path).Contains(ActionUpdate.Instance.Letter.ToString())) + if (CurrentUser.GetPermissions(dd.Path).Contains(ActionUpdate.Instance.Letter.ToString(CultureInfo.InvariantCulture))) { treeElement.Action = String.Format("javascript:openContent({0});", dd.Id); } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs index 9aed597702..f8fc1d3ea3 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs @@ -137,8 +137,8 @@ namespace umbraco.dialogs private bool CheckPermissions(IContentBase node, IAction currentAction) { var userService = ApplicationContext.Current.Services.UserService; - var currUserPermissions = userService.GetPermissions(UmbracoContext.Current.Security.CurrentUser, node.Id).FirstOrDefault(); - return currUserPermissions != null && currUserPermissions.AssignedPermissions.Contains(currentAction.Letter.ToString()); + var currUserPermissions = userService.GetPermissions(UmbracoContext.Current.Security.CurrentUser, node.Id).GetAllPermissions(); + return currUserPermissions != null && currUserPermissions.Contains(currentAction.Letter.ToString(CultureInfo.InvariantCulture)); } private void HandleDocumentTypeCopy() diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/notifications.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/notifications.aspx.cs index 97dc8d84d3..70137da920 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/notifications.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/notifications.aspx.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Globalization; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; @@ -49,7 +50,7 @@ namespace umbraco.dialogs { CheckBox c = new CheckBox(); - c.ID = a.Letter.ToString(); + c.ID = a.Letter.ToString(CultureInfo.InvariantCulture); if (base.getUser().GetNotifications(node.Path).IndexOf(a.Letter) > -1) c.Checked = true; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs index 6d0465f693..346008bda3 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.ComponentModel; +using System.Globalization; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; @@ -304,7 +305,7 @@ namespace umbraco.presentation.templateControls protected virtual bool FieldEditableWithUserPermissions() { BusinessLogic.User u = helper.GetCurrentUmbracoUser(); - return u != null && u.GetPermissions(PageElements["path"].ToString()).Contains(ActionUpdate.Instance.Letter.ToString()); + return u != null && u.GetPermissions(PageElements["path"].ToString()).Contains(ActionUpdate.Instance.Letter.ToString(CultureInfo.InvariantCulture)); } #endregion diff --git a/src/umbraco.cms/Actions/Action.cs b/src/umbraco.cms/Actions/Action.cs index 3a5503d714..81c78ade1b 100644 --- a/src/umbraco.cms/Actions/Action.cs +++ b/src/umbraco.cms/Actions/Action.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Web; using System.Reflection; using Umbraco.Core; @@ -150,7 +151,7 @@ namespace umbraco.BusinessLogic.Actions /// public static string ToString(List actions) { - string[] strMenu = Array.ConvertAll(actions.ToArray(), delegate(IAction a) { return (a.Letter.ToString()); }); + string[] strMenu = Array.ConvertAll(actions.ToArray(), delegate(IAction a) { return (a.Letter.ToString(CultureInfo.InvariantCulture)); }); return string.Join("", strMenu); } From 6a48c1646d3ed3844bb70f96407c2fdad151bcff Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 13 Jul 2017 09:26:48 +0200 Subject: [PATCH 385/714] checking for uses of userType. --- .../Repositories/UserGroupRepository.cs | 2 +- .../Repositories/UserGroupRepositoryTest.cs | 22 ++++++++--------- .../Repositories/UserRepositoryTest.cs | 24 +++++++++---------- .../Providers/UsersMembershipProvider.cs | 4 ++-- .../members/UmbracoMembershipProvider.cs | 2 +- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index 7a160f7f24..02f62101ff 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -125,7 +125,7 @@ namespace Umbraco.Core.Persistence.Repositories repo.AssignPermission(groupId, permission, entityIds); } - #region Overrides of RepositoryBase + #region Overrides of RepositoryBase protected override IUserGroup PerformGet(int id) { diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs index 7bf29bb0ab..57d1f80995 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserGroupRepositoryTest.cs @@ -64,18 +64,18 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repository = CreateRepository(unitOfWork)) { - var userType1 = MockedUserGroup.CreateUserGroup("1"); - var userType2 = MockedUserGroup.CreateUserGroup("2"); + var userGroup1 = MockedUserGroup.CreateUserGroup("1"); + var userGroup2 = MockedUserGroup.CreateUserGroup("2"); // Act - repository.AddOrUpdate(userType1); + repository.AddOrUpdate(userGroup1); unitOfWork.Commit(); - repository.AddOrUpdate(userType2); + repository.AddOrUpdate(userGroup2); unitOfWork.Commit(); // Assert - Assert.That(userType1.HasIdentity, Is.True); - Assert.That(userType2.HasIdentity, Is.True); + Assert.That(userGroup1.HasIdentity, Is.True); + Assert.That(userGroup2.HasIdentity, Is.True); } } @@ -209,10 +209,10 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repository = CreateRepository(unitOfWork)) { - var userTypes = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + var userGroups = CreateAndCommitMultipleUserGroups(repository, unitOfWork); // Act - var result = repository.GetAll(userTypes[0].Id, userTypes[1].Id); + var result = repository.GetAll(userGroups[0].Id, userGroups[1].Id); // Assert Assert.That(result, Is.Not.Null); @@ -249,10 +249,10 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repository = CreateRepository(unitOfWork)) { - var userTypes = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + var userGroups = CreateAndCommitMultipleUserGroups(repository, unitOfWork); // Act - var exists = repository.Exists(userTypes[0].Id); + var exists = repository.Exists(userGroups[0].Id); // Assert Assert.That(exists, Is.True); @@ -267,7 +267,7 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); using (var repository = CreateRepository(unitOfWork)) { - var userTypes = CreateAndCommitMultipleUserGroups(repository, unitOfWork); + var userGroups = CreateAndCommitMultipleUserGroups(repository, unitOfWork); // Act var query = Query.Builder.Where(x => x.Alias == "testUserGroup1" || x.Alias == "testUserGroup2"); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 88ecb50f2f..334c4f98c2 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -36,7 +36,7 @@ namespace Umbraco.Tests.Persistence.Repositories var tagRepository = new TagRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); return repository; - } + } private ContentRepository CreateContentRepository(IScopeUnitOfWork unitOfWork, out IContentTypeRepository contentTypeRepository) { @@ -51,7 +51,7 @@ namespace Umbraco.Tests.Persistence.Repositories contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); return repository; - } + } private UserRepository CreateRepository(IScopeUnitOfWork unitOfWork) { @@ -86,7 +86,7 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Can_Perform_Add_With_Group() { - var group = MockedUserGroup.CreateUserGroup(); + var group = MockedUserGroup.CreateUserGroup(); // Arrange var provider = new PetaPocoUnitOfWorkProvider(Logger); @@ -95,22 +95,22 @@ namespace Umbraco.Tests.Persistence.Repositories { repository.AddOrUpdate(group); unitOfWork.Commit(); - } + } using (var repository = CreateRepository(unitOfWork)) { IUser user = MockedUser.CreateUser(); - user.AddGroup(group.ToReadOnlyGroup()); + user.AddGroup(group.ToReadOnlyGroup()); // Act repository.AddOrUpdate(user); unitOfWork.Commit(); - user = repository.Get(user.Id); + user = repository.Get(user.Id); // Assert Assert.That(user.HasIdentity, Is.True); - Assert.AreEqual(1, user.Groups.Count()); + Assert.AreEqual(1, user.Groups.Count()); Assert.AreEqual(group.Alias, user.Groups.ElementAt(0).Alias); } } @@ -165,7 +165,7 @@ namespace Umbraco.Tests.Persistence.Repositories var ct = MockedContentTypes.CreateBasicContentType("test"); var content = MockedContent.CreateBasicContent(ct); var mt = MockedContentTypes.CreateSimpleMediaType("testmedia", "TestMedia"); - var media = MockedMedia.CreateSimpleMedia(mt, "asdf", -1); + var media = MockedMedia.CreateSimpleMedia(mt, "asdf", -1); // Arrange var provider = new PetaPocoUnitOfWorkProvider(Logger); @@ -173,8 +173,8 @@ namespace Umbraco.Tests.Persistence.Repositories IContentTypeRepository contentTypeRepo; IMediaTypeRepository mediaTypeRepo; using (var contentRepository = CreateContentRepository(unitOfWork, out contentTypeRepo)) - using (var mediaRepository = CreateMediaRepository(unitOfWork, out mediaTypeRepo)) - using (contentTypeRepo) + using (var mediaRepository = CreateMediaRepository(unitOfWork, out mediaTypeRepo)) + using (contentTypeRepo) using(mediaTypeRepo) using (var userRepository = CreateRepository(unitOfWork)) using (var userGroupRepository = CreateUserGroupRepository(unitOfWork)) @@ -185,7 +185,7 @@ namespace Umbraco.Tests.Persistence.Repositories contentRepository.AddOrUpdate(content); mediaRepository.AddOrUpdate(media); - unitOfWork.Commit(); + unitOfWork.Commit(); var user = CreateAndCommitUserWithGroup(userRepository, userGroupRepository, unitOfWork); @@ -422,7 +422,7 @@ namespace Umbraco.Tests.Persistence.Repositories var group = MockedUserGroup.CreateUserGroup(); userGroupRepository.AddOrUpdateGroupWithUsers(@group, new[] {user.Id}); - unitOfWork.Commit(); + unitOfWork.Commit(); return user; } diff --git a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs index cd19d36f2d..7034101a83 100644 --- a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs @@ -46,8 +46,8 @@ namespace Umbraco.Web.Security.Providers { base.Initialize(name, config); - // test for membertype (if not specified, choose the first member type available) - // We'll support both names for legacy reasons: defaultUserTypeAlias & defaultUserGroupAlias + // test for membertype (if not specified, choose the first member type available) + // We'll support both names for legacy reasons: defaultUserTypeAlias & defaultUserGroupAlias if (config["defaultUserTypeAlias"] != null) { diff --git a/src/umbraco.providers/members/UmbracoMembershipProvider.cs b/src/umbraco.providers/members/UmbracoMembershipProvider.cs index 2a563d27ce..648bf5a0d5 100644 --- a/src/umbraco.providers/members/UmbracoMembershipProvider.cs +++ b/src/umbraco.providers/members/UmbracoMembershipProvider.cs @@ -126,7 +126,7 @@ namespace umbraco.providers.members _defaultMemberTypeAlias = config["defaultMemberTypeAlias"]; if (_defaultMemberTypeAlias.IsNullOrWhiteSpace()) { - throw new ProviderException("No default user group alias is specified in the web.config string. Please add a 'defaultUserTypeAlias' to the add element in the provider declaration in web.config"); + throw new ProviderException("No default MemberType alias is specified in the web.config string. Please add a 'defaultMemberTypeAlias' to the add element in the provider declaration in web.config"); } _hasDefaultMember = true; } From 1643eec62c432434ba37ad0581b0a838ed3d73a1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Jul 2017 18:47:10 +1000 Subject: [PATCH 386/714] Fixes cache refreshing for user groups since user cache need to be cleared for that too. Updates cache refreshers to use json.net for serializing which is way faster. adds better support for querying multiple user groups and entity ids for permissions, updates tests --- .../Models/Membership/EntityPermissionSet.cs | 6 +- .../Repositories/PermissionRepository.cs | 3 + .../Repositories/UserGroupRepository.cs | 8 +- src/Umbraco.Core/Services/IUserService.cs | 24 ++-- src/Umbraco.Core/Services/UserService.cs | 133 ++++++++++++------ .../Services/UserServiceExtensions.cs | 33 ++++- .../Services/ContentServiceTests.cs | 17 ++- .../Services/UserServiceTests.cs | 3 +- .../Cache/CacheRefresherEventHandler.cs | 27 +++- .../Cache/ContentTypeCacheRefresher.cs | 7 +- .../Cache/DataTypeCacheRefresher.cs | 7 +- .../Cache/DistributedCacheExtensions.cs | 19 +++ src/Umbraco.Web/Cache/MacroCacheRefresher.cs | 13 +- src/Umbraco.Web/Cache/MediaCacheRefresher.cs | 13 +- .../Cache/MemberGroupCacheRefresher.cs | 7 +- .../Cache/UnpublishedPageCacheRefresher.cs | 8 +- .../Cache/UserGroupCacheRefresher.cs | 12 +- .../Models/Mapping/UserModelMapper.cs | 2 +- src/umbraco.cms/businesslogic/Permission.cs | 132 +---------------- 19 files changed, 233 insertions(+), 241 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs index 0f35fec990..c33c4aa315 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace Umbraco.Core.Models.Membership @@ -8,13 +9,14 @@ namespace Umbraco.Core.Models.Membership /// public class EntityPermissionSet { + private static readonly Lazy EmptyInstance = new Lazy(() => new EntityPermissionSet(-1, new EntityPermissionCollection())); /// /// Returns an empty permission set /// /// public static EntityPermissionSet Empty() { - return new EntityPermissionSet(-1, new EntityPermissionCollection()); + return EmptyInstance.Value; } public EntityPermissionSet(int entityId, EntityPermissionCollection permissionsSet) diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index 7dc6996b57..edf0274832 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -135,6 +135,9 @@ namespace Umbraco.Core.Persistence.Repositories /// public void ReplacePermissions(int groupId, IEnumerable permissions, params int[] entityIds) { + if (entityIds.Length == 0) + return; + var db = UnitOfWork.Database; //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit diff --git a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs index 137cc0885b..3d291d5e07 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserGroupRepository.cs @@ -132,6 +132,10 @@ namespace Umbraco.Core.Persistence.Repositories { foreach (var nodeId in nodeIds) { + //TODO: We could/should change the EntityPermissionsCollection into a KeyedCollection and they key could be + // a struct of the nodeid + groupid so then we don't actually allocate this class just to check if it's not + // going to be included in the result! + var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions.ToArray(), isDefaultPermissions: true); //Since this is a hashset, this will not add anything that already exists by group/node combination result.Add(defaultPermission); @@ -145,8 +149,8 @@ namespace Umbraco.Core.Persistence.Repositories /// Replaces the same permission set for a single group to any number of entities /// /// Id of group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. + /// Permissions as enumerable list of If nothing is specified all permissions are removed. + /// Specify the nodes to replace permissions for. public void ReplaceGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds) { _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 5ae31f8c0d..ea56264075 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -95,35 +95,35 @@ namespace Umbraco.Core.Services /// /// This will return the default permissions for the user's groups for node ids that don't have explicitly defined permissions /// - EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds); - + EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds); + /// - /// Get explicitly assigned permissions for a group and optional node Ids + /// Get explicitly assigned permissions for groups and optional node Ids /// - /// Group to retrieve permissions for + /// /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - EntityPermissionCollection GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds); + EntityPermissionCollection GetPermissions(IUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds); /// /// Gets the implicit/inherited permissions for the user for the given path /// /// User to check permissions for /// Path to check permissions for - EntityPermissionSet GetPermissionsForPath(IUser user, string path); - + EntityPermissionSet GetPermissionsForPath(IUser user, string path); + /// - /// Gets the permissions for the provided group and path + /// Gets the permissions for the provided groups and path /// - /// Group to check permissions for + /// /// Path to check permissions for /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// - EntityPermission GetPermissionsForPath(IUserGroup group, string path, bool fallbackToDefaultPermissions = false); + EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false); /// /// Replaces the same permission set for a single group to any number of entities diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 24ce233cc2..2d5fff536c 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data.Common; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using Umbraco.Core.Configuration; @@ -680,15 +681,26 @@ namespace Umbraco.Core.Services /// /// If no 'entityIds' are specified all permissions will be removed for the specified group. /// Id of the group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. + /// Permissions as enumerable list of If nothing is specified all permissions are removed. + /// Specify the nodes to replace permissions for. public void ReplaceUserGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds) { + if (entityIds.Length == 0) + return; + using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); repository.ReplaceGroupPermissions(groupId, permissions, entityIds); uow.Commit(); + + uow.Events.Dispatch(UserGroupPermissionsAssigned, this, new SaveEventArgs( + entityIds.Select( + x => new EntityPermission( + groupId, + x, + permissions.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray())) + .ToArray(), false)); } } @@ -700,11 +712,22 @@ namespace Umbraco.Core.Services /// Specify the nodes to replace permissions for public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) { + if (entityIds.Length == 0) + return; + using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); repository.AssignGroupPermission(groupId, permission, entityIds); uow.Commit(); + + uow.Events.Dispatch(UserGroupPermissionsAssigned, this, new SaveEventArgs( + entityIds.Select( + x => new EntityPermission( + groupId, + x, + new[] {permission.ToString(CultureInfo.InvariantCulture)})) + .ToArray(), false)); } } @@ -853,45 +876,45 @@ namespace Umbraco.Core.Services var repository = RepositoryFactory.CreateUserGroupRepository(uow); return repository.GetPermissions(user.Groups.ToArray(), true, nodeIds); - } + } } /// /// Get explicitly assigned permissions for a group and optional node Ids /// - /// Group to retrieve permissions for - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// Specifiying nothing will return all permissions for all nodes - /// An enumerable list of - private IEnumerable GetPermissions(IReadOnlyUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (group == null) throw new ArgumentNullException("group"); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateUserGroupRepository(uow); - return repository.GetPermissions(new[] { group }, fallbackToDefaultPermissions, nodeIds); - } - } - - /// - /// Get explicitly assigned permissions for a group and optional node Ids - /// - /// Group to retrieve permissions for + /// Groups to retrieve permissions for /// /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - public EntityPermissionCollection GetPermissions(IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) + private IEnumerable GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) { - if (group == null) throw new ArgumentNullException("group"); + if (groups == null) throw new ArgumentNullException("group"); + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateUserGroupRepository(uow); - return repository.GetPermissions(new[] { group.ToReadOnlyGroup() }, fallbackToDefaultPermissions, nodeIds); + return repository.GetPermissions(groups, fallbackToDefaultPermissions, nodeIds); + } + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + public EntityPermissionCollection GetPermissions(IUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) throw new ArgumentNullException("groups"); + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateUserGroupRepository(uow); + return repository.GetPermissions(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), fallbackToDefaultPermissions, nodeIds); } } @@ -905,36 +928,47 @@ namespace Umbraco.Core.Services var nodeIds = path.GetIdsFromPathReversed(); if (nodeIds.Length == 0) - return EntityPermissionSet.Empty(); - - //collect all permissions structures for all nodes for all groups belonging to the user - var groupPermissions = user.Groups - .Select(g => GetPermissionsForPath(g, nodeIds, fallbackToDefaultPermissions: true)) - .ToArray(); - + return EntityPermissionSet.Empty(); + + //collect all permissions structures for all nodes for all groups belonging to the user + var groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); - } - + } + /// /// Gets the permissions for the provided group and path /// - /// Group to check permissions for + /// /// Path to check permissions for /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// String indicating permissions for provided user and path - public EntityPermission GetPermissionsForPath(IUserGroup group, string path, bool fallbackToDefaultPermissions = false) + public EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false) { var nodeIds = path.GetIdsFromPathReversed(); - return GetPermissionsForPath(group.ToReadOnlyGroup(), nodeIds, fallbackToDefaultPermissions); + + if (nodeIds.Length == 0) + return EntityPermissionSet.Empty(); + + //collect all permissions structures for all nodes for all groups + var groupPermissions = GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); } - private EntityPermission GetPermissionsForPath(IReadOnlyUserGroup group, int[] pathIds, bool fallbackToDefaultPermissions = false) + private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) { - //get permissions for all nodes in the path - var permissions = GetPermissions(group, fallbackToDefaultPermissions, pathIds); - return GetPermissionsForPathForGroup(permissions, pathIds, fallbackToDefaultPermissions); + if (pathIds.Length == 0) + return new EntityPermissionCollection(Enumerable.Empty()); + + //get permissions for all nodes in the path by group + var permissions = GetPermissions(groups, fallbackToDefaultPermissions, pathIds) + .GroupBy(x => x.UserGroupId); + + return new EntityPermissionCollection( + permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions))); } /// @@ -1011,7 +1045,10 @@ namespace Umbraco.Core.Services /// /// Returns the resulting permission set for a group for the path based on all permissions provided for the branch /// - /// The collective set of permissions provided to calculate the resulting permissions set for the path + /// + /// The collective set of permissions provided to calculate the resulting permissions set for the path + /// based on a single group + /// /// Must be ordered deepest to shallowest (right to left) /// /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set @@ -1122,6 +1159,10 @@ namespace Umbraco.Core.Services /// /// Occurs after Delete /// - public static event TypedEventHandler> DeletedUserGroup; + public static event TypedEventHandler> DeletedUserGroup; + + //TODO: still don't know if we need this yet unless we start caching permissions, but that also means we'll need another + // event on the ContentService since there's a method there to modify node permissions too, or we can proxy events if needed. + internal static event TypedEventHandler> UserGroupPermissionsAssigned; } } diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 0492a59d03..e53a24527f 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -4,8 +4,37 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Services { - internal static class UserServiceExtensions + public static class UserServiceExtensions { + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + public static EntityPermissionCollection GetPermissions(this IUserService service, IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + return service.GetPermissions(new[] {group}, fallbackToDefaultPermissions, nodeIds); + } + + /// + /// Gets the permissions for the provided group and path + /// + /// + /// + /// Path to check permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + public static EntityPermissionSet GetPermissionsForPath(this IUserService service, IUserGroup group, string path, bool fallbackToDefaultPermissions = false) + { + return service.GetPermissionsForPath(new[] { group }, path, fallbackToDefaultPermissions); + } + /// /// Remove all permissions for this user group for all nodes specified /// @@ -36,7 +65,7 @@ namespace Umbraco.Core.Services /// To maintain compatibility we have to check the login name if the provider key lookup fails but otherwise /// we'll store the provider user key in the login column. /// - public static IUser CreateUserMappingForCustomProvider(this IUserService userService, MembershipUser member) + internal static IUser CreateUserMappingForCustomProvider(this IUserService userService, MembershipUser member) { if (member == null) throw new ArgumentNullException("member"); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 43f049adf4..69ff951f26 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1488,8 +1488,9 @@ namespace Umbraco.Tests.Services //get the permissions and verify var permissions = ServiceContext.UserService.GetPermissionsForPath(userGroup, copy.Path, fallbackToDefaultPermissions: true); - Assert.AreEqual(1, permissions.AssignedPermissions.Length); - Assert.AreEqual("A", permissions.AssignedPermissions[0]); + var allPermissions = permissions.GetAllPermissions().ToArray(); + Assert.AreEqual(1, allPermissions.Length); + Assert.AreEqual("A", allPermissions[0]); } [Test] @@ -1523,9 +1524,10 @@ namespace Umbraco.Tests.Services foreach (var descendant in descendants) { - var permissions = ServiceContext.UserService.GetPermissionsForPath(userGroup, descendant.Path, fallbackToDefaultPermissions:true); - Assert.AreEqual(1, permissions.AssignedPermissions.Length); - Assert.AreEqual("A", permissions.AssignedPermissions[0]); + var permissions = ServiceContext.UserService.GetPermissionsForPath(userGroup, descendant.Path, fallbackToDefaultPermissions: true); + var allPermissions = permissions.GetAllPermissions().ToArray(); + Assert.AreEqual(1, allPermissions.Length); + Assert.AreEqual("A", allPermissions[0]); } //create a new parent with a new permission structure @@ -1542,8 +1544,9 @@ namespace Umbraco.Tests.Services foreach (var descendant in descendants) { var permissions = ServiceContext.UserService.GetPermissionsForPath(userGroup, descendant.Path, fallbackToDefaultPermissions: true); - Assert.AreEqual(1, permissions.AssignedPermissions.Length); - Assert.AreEqual("B", permissions.AssignedPermissions[0]); + var allPermissions = permissions.GetAllPermissions().ToArray(); + Assert.AreEqual(1, allPermissions.Length); + Assert.AreEqual("B", allPermissions[0]); } } diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index cdfff6ff0d..dcd572fdd7 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -432,7 +432,8 @@ namespace Umbraco.Tests.Services var permissions = userService.GetPermissionsForPath(userGroup, child2.Path); //assert - Assert.AreEqual(3, permissions.AssignedPermissions.Length); + var allPermissions = permissions.GetAllPermissions().ToArray(); + Assert.AreEqual(3, allPermissions.Length); } [Test] diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index daf405a562..b4e28fb95b 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -68,6 +68,8 @@ namespace Umbraco.Web.Cache () => UserService.SavedUser -= UserService_SavedUser); Bind(() => UserService.DeletedUser += UserService_DeletedUser, () => UserService.DeletedUser -= UserService_DeletedUser); + Bind(() => UserService.UserGroupPermissionsAssigned += UserService_UserGroupPermissionsAssigned, + () => UserService.UserGroupPermissionsAssigned -= UserService_UserGroupPermissionsAssigned); // bind to dictionary events Bind(() => LocalizationService.DeletedDictionaryItem += LocalizationService_DeletedDictionaryItem, @@ -111,8 +113,10 @@ namespace Umbraco.Web.Cache Bind(() => MemberTypeService.Saved += MemberTypeService_Saved, () => MemberTypeService.Saved -= MemberTypeService_Saved); Bind(() => MemberTypeService.Deleted += MemberTypeService_Deleted, - () => MemberTypeService.Deleted -= MemberTypeService_Deleted); - + () => MemberTypeService.Deleted -= MemberTypeService_Deleted); + + + // bind to template events Bind(() => FileService.SavedTemplate += FileService_SavedTemplate, () => FileService.SavedTemplate -= FileService_SavedTemplate); @@ -548,13 +552,22 @@ namespace Umbraco.Web.Cache static void MemberTypeService_Saved(IMemberTypeService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshMemberTypeCache(x)); - } - - + } + + #endregion - + #region User/permissions event handlers - + + static void UserService_UserGroupPermissionsAssigned(IUserService sender, SaveEventArgs e) + { + //TODO: Not sure if we need this yet depends if we start caching permissions + //var groupIds = e.SavedEntities.Select(x => x.UserGroupId).Distinct(); + //foreach (var groupId in groupIds) + //{ + // DistributedCache.Instance.RefreshUserGroupPermissionsCache(groupId); + //} + } static void UserService_SavedUser(IUserService sender, SaveEventArgs e) { diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index e83050dd26..5e527cdfcb 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Script.Serialization; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; @@ -34,8 +35,7 @@ namespace Umbraco.Web.Cache /// internal static JsonPayload[] DeserializeFromJsonPayload(string json) { - var serializer = new JavaScriptSerializer(); - var jsonObject = serializer.Deserialize(json); + var jsonObject = JsonConvert.DeserializeObject(json); return jsonObject; } @@ -79,9 +79,8 @@ namespace Umbraco.Web.Cache /// internal static string SerializeToJsonPayload(bool isDeleted, params IContentTypeBase[] contentTypes) { - var serializer = new JavaScriptSerializer(); var items = contentTypes.Select(x => FromContentType(x, isDeleted)).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 9d8dfc3332..833855a972 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -3,6 +3,7 @@ using System.Web.Script.Serialization; using Umbraco.Core; using Umbraco.Core.Cache; using System.Linq; +using Newtonsoft.Json; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; @@ -27,8 +28,7 @@ namespace Umbraco.Web.Cache /// private static JsonPayload[] DeserializeFromJsonPayload(string json) { - var serializer = new JavaScriptSerializer(); - var jsonObject = serializer.Deserialize(json); + var jsonObject = JsonConvert.DeserializeObject(json); return jsonObject; } @@ -39,9 +39,8 @@ namespace Umbraco.Web.Cache /// internal static string SerializeToJsonPayload(params IDataTypeDefinition[] dataTypes) { - var serializer = new JavaScriptSerializer(); var items = dataTypes.Select(FromDataTypeDefinition).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 397410dbd5..849cd6c81a 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -79,6 +79,25 @@ namespace Umbraco.Web.Cache #endregion + #region User group permissions cache + + public static void RemoveUserGroupPermissionsCache(this DistributedCache dc, int groupId) + { + dc.Remove(DistributedCache.UserGroupPermissionsCacheRefresherGuid, groupId); + } + + public static void RefreshUserGroupPermissionsCache(this DistributedCache dc, int groupId) + { + //TODO: Not sure if we need this yet depends if we start caching permissions + //dc.Refresh(DistributedCache.UserGroupPermissionsCacheRefresherGuid, groupId); + } + + public static void RefreshAllUserGroupPermissionsCache(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.UserGroupPermissionsCacheRefresherGuid); + } + + #endregion #region Template cache diff --git a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs index cb62dbeb39..4292a30589 100644 --- a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs @@ -8,6 +8,7 @@ using umbraco; using Umbraco.Core.Persistence.Repositories; using umbraco.interfaces; using System.Linq; +using Newtonsoft.Json; using Macro = umbraco.cms.businesslogic.macro.Macro; namespace Umbraco.Web.Cache @@ -47,8 +48,7 @@ namespace Umbraco.Web.Cache /// private static JsonPayload[] DeserializeFromJsonPayload(string json) { - var serializer = new JavaScriptSerializer(); - var jsonObject = serializer.Deserialize(json); + var jsonObject = JsonConvert.DeserializeObject(json); return jsonObject; } @@ -59,9 +59,8 @@ namespace Umbraco.Web.Cache /// internal static string SerializeToJsonPayload(params Macro[] macros) { - var serializer = new JavaScriptSerializer(); var items = macros.Select(FromMacro).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } @@ -72,9 +71,8 @@ namespace Umbraco.Web.Cache /// internal static string SerializeToJsonPayload(params IMacro[] macros) { - var serializer = new JavaScriptSerializer(); var items = macros.Select(FromMacro).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } @@ -85,9 +83,8 @@ namespace Umbraco.Web.Cache /// internal static string SerializeToJsonPayload(params macro[] macros) { - var serializer = new JavaScriptSerializer(); var items = macros.Select(FromMacro).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index fc589b530b..3613da426d 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using umbraco.interfaces; using System.Linq; +using Newtonsoft.Json; using Umbraco.Web.PublishedCache.XmlPublishedCache; namespace Umbraco.Web.Cache @@ -32,8 +33,7 @@ namespace Umbraco.Web.Cache /// public static JsonPayload[] DeserializeFromJsonPayload(string json) { - var serializer = new JavaScriptSerializer(); - var jsonObject = serializer.Deserialize(json); + var jsonObject = JsonConvert.DeserializeObject(json); return jsonObject; } @@ -45,34 +45,31 @@ namespace Umbraco.Web.Cache /// internal static string SerializeToJsonPayload(OperationType operation, params IMedia[] media) { - var serializer = new JavaScriptSerializer(); var items = media.Select(x => FromMedia(x, operation)).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } internal static string SerializeToJsonPayloadForMoving(OperationType operation, MoveEventInfo[] media) { - var serializer = new JavaScriptSerializer(); var items = media.Select(x => new JsonPayload { Id = x.Entity.Id, Operation = operation, Path = x.OriginalPath }).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] mediaIds) { - var serializer = new JavaScriptSerializer(); var items = mediaIds.Select(x => new JsonPayload { Id = x, Operation = OperationType.Deleted }).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } diff --git a/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs index dc2ba39b9d..2d61b254a0 100644 --- a/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Web.Script.Serialization; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; @@ -20,8 +21,7 @@ namespace Umbraco.Web.Cache /// private static JsonPayload[] DeserializeFromJsonPayload(string json) { - var serializer = new JavaScriptSerializer(); - var jsonObject = serializer.Deserialize(json); + var jsonObject = JsonConvert.DeserializeObject(json); return jsonObject; } @@ -32,9 +32,8 @@ namespace Umbraco.Web.Cache /// internal static string SerializeToJsonPayload(params IMemberGroup[] groups) { - var serializer = new JavaScriptSerializer(); var items = groups.Select(FromMemberGroup).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs index e352c8108a..2941357f19 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -5,7 +5,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; using System.Linq; - +using Newtonsoft.Json; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Sync; @@ -40,21 +40,19 @@ namespace Umbraco.Web.Cache /// internal static JsonPayload[] DeserializeFromJsonPayload(string json) { - var serializer = new JavaScriptSerializer(); - var jsonObject = serializer.Deserialize(json); + var jsonObject = JsonConvert.DeserializeObject(json); return jsonObject; } internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] contentIds) { - var serializer = new JavaScriptSerializer(); var items = contentIds.Select(x => new JsonPayload { Id = x, Operation = OperationType.Deleted }).ToArray(); - var json = serializer.Serialize(items); + var json = JsonConvert.SerializeObject(items); return json; } diff --git a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs index 84496b61fd..5fd1e4dfa4 100644 --- a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs @@ -9,6 +9,9 @@ namespace Umbraco.Web.Cache /// /// Handles User group cache invalidation/refreshing /// + /// + /// This also needs to clear the user cache since IReadOnlyUserGroup's are attached to IUser objects + /// public sealed class UserGroupCacheRefresher : CacheRefresherBase { protected override UserGroupCacheRefresher Instance @@ -35,6 +38,9 @@ namespace Umbraco.Web.Cache userGroupCache.Result.ClearCacheByKeySearch(UserGroupRepository.GetByAliasCacheKeyPrefix); } + //We'll need to clear all user cache too + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); } @@ -52,8 +58,12 @@ namespace Umbraco.Web.Cache userGroupCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); userGroupCache.Result.ClearCacheByKeySearch(UserGroupRepository.GetByAliasCacheKeyPrefix); } - + + //we don't know what user's belong to this group without doing a look up so we'll need to just clear them all + ClearAllIsolatedCacheByEntityType(); + base.Remove(id); } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 8056724a68..6525e95d66 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -193,7 +193,7 @@ namespace Umbraco.Web.Models.Mapping //Deal with assigned permissions: - var allContentPermissions = applicationContext.Services.UserService.GetPermissions(group, true) + var allContentPermissions = applicationContext.Services.UserService.GetPermissions(@group, true) .ToDictionary(x => x.EntityId, x => x); var contentEntities = allContentPermissions.Keys.Count == 0 diff --git a/src/umbraco.cms/businesslogic/Permission.cs b/src/umbraco.cms/businesslogic/Permission.cs index 335535c08d..32daa3df50 100644 --- a/src/umbraco.cms/businesslogic/Permission.cs +++ b/src/umbraco.cms/businesslogic/Permission.cs @@ -32,24 +32,8 @@ namespace umbraco.BusinessLogic public static void MakeNew(IUserGroup userGroup, CMSNode node, char permissionKey) { - MakeNew(userGroup, node, permissionKey, true); - } - - private static void MakeNew(IUserGroup userGroup, IEnumerable nodes, char permissionKey, bool raiseEvents) - { - var asArray = nodes.ToArray(); - - ApplicationContext.Current.Services.UserService.AssignUserGroupPermission(userGroup.Id, permissionKey, asArray.Select(x => x.Id).ToArray()); - - if (raiseEvents) - { - OnNew(new UserGroupPermission(userGroup, asArray, new[] { permissionKey }), new NewEventArgs()); - } - } - - private static void MakeNew(IUserGroup userGroup, CMSNode Node, char PermissionKey, bool raiseEvents) - { - MakeNew(userGroup, new[] {Node}, PermissionKey, raiseEvents); + ApplicationContext.Current.Services.UserService.AssignUserGroupPermission( + userGroup.Id, permissionKey, node.Id); } /// @@ -99,17 +83,8 @@ namespace umbraco.BusinessLogic /// /// public static void DeletePermissions(IUserGroup userGroup, CMSNode node) - { - DeletePermissions(userGroup, node, true); - } - - internal static void DeletePermissions(IUserGroup userGroup, CMSNode node, bool raiseEvents) { ApplicationContext.Current.Services.UserService.RemoveUserGroupPermissions(userGroup.Id, node.Id); - if (raiseEvents) - { - OnDeleted(new UserGroupPermission(userGroup, node, null), new DeleteEventArgs()); - } } /// @@ -119,15 +94,13 @@ namespace umbraco.BusinessLogic public static void DeletePermissions(IUserGroup userGroup) { ApplicationContext.Current.Services.UserService.RemoveUserGroupPermissions(userGroup.Id); - - OnDeleted(new UserGroupPermission(userGroup, Enumerable.Empty(), null), new DeleteEventArgs()); + } public static void DeletePermissions(int userGroupId, int[] iNodeIDs) { ApplicationContext.Current.Services.UserService.RemoveUserGroupPermissions(userGroupId, iNodeIDs); - - OnDeleted(new UserGroupPermission(userGroupId, iNodeIDs), new DeleteEventArgs()); + } public static void DeletePermissions(int userGroupId, int iNodeID) { @@ -142,7 +115,6 @@ namespace umbraco.BusinessLogic { ApplicationContext.Current.Services.ContentService.RemoveContentPermissions(node.Id); - OnDeleted(new UserGroupPermission(null, node, null), new DeleteEventArgs()); } public static void UpdateCruds(IUserGroup userGroup, CMSNode node, string permissions) @@ -151,103 +123,9 @@ namespace umbraco.BusinessLogic userGroup.Id, permissions.ToCharArray(), node.Id); - - OnUpdated(new UserGroupPermission(userGroup, node, permissions.ToCharArray()), new SaveEventArgs()); - } - - internal static event TypedEventHandler Deleted; - private static void OnDeleted(UserGroupPermission permission, DeleteEventArgs args) - { - if (Deleted != null) - { - Deleted(permission, args); - } - } - - internal static event TypedEventHandler Updated; - private static void OnUpdated(UserGroupPermission permission, SaveEventArgs args) - { - if (Updated != null) - { - Updated(permission, args); - } - } - - internal static event TypedEventHandler New; - private static void OnNew(UserGroupPermission permission, NewEventArgs args) - { - if (New != null) - { - New(permission, args); - } + } } - internal class UserGroupPermission - { - private int? _userGroupId; - private readonly int[] _nodeIds; - - internal UserGroupPermission(int userGroupId) - { - _userGroupId = userGroupId; - } - - internal UserGroupPermission(int userGroupId, IEnumerable nodeIds) - { - _userGroupId = userGroupId; - _nodeIds = nodeIds.ToArray(); - } - - internal UserGroupPermission(IUserGroup userGroup, CMSNode node, char[] permissionKeys) - { - UserGroup = userGroup; - Nodes = new[] { node }; - PermissionKeys = permissionKeys; - } - - internal UserGroupPermission(IUserGroup userGroup, IEnumerable nodes, char[] permissionKeys) - { - UserGroup = userGroup; - Nodes = nodes; - PermissionKeys = permissionKeys; - } - - internal int UserId - { - get - { - if (_userGroupId.HasValue) - { - return _userGroupId.Value; - } - if (UserGroup != null) - { - return UserGroup.Id; - } - return -1; - } - } - - internal IEnumerable NodeIds - { - get - { - if (_nodeIds != null) - { - return _nodeIds; - } - if (Nodes != null) - { - return Nodes.Select(x => x.Id); - } - return Enumerable.Empty(); - } - } - - internal IUserGroup UserGroup { get; private set; } - internal IEnumerable Nodes { get; private set; } - internal char[] PermissionKeys { get; private set; } - } } \ No newline at end of file From ab1dade4684f0a9710653df2c2755a8139307599 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 13 Jul 2017 13:11:46 +0200 Subject: [PATCH 387/714] U4-10121 - properly dispose context --- src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs | 6 ++++-- src/Umbraco.Web/Scheduling/ScheduledPublishing.cs | 9 ++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index df25d9dfe0..3ebfe25ff8 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -797,6 +797,9 @@ namespace Umbraco.Web.Cache //TODO: We should remove this in v8, this is a backwards compat hack and is needed because when we are using Deploy, the events will be raised on a background //thread which means that cache refreshers will also execute on a background thread and in many cases developers may be using UmbracoContext.Current in their //cache refresher handlers, so before we execute all of the events, we'll ensure a context + // + // note that this means we must *exclusively* use this method in a background task else we'd replace and dispose a real context + // UmbracoContext tempContext = null; if (UmbracoContext.Current == null) { @@ -823,9 +826,8 @@ namespace Umbraco.Web.Cache finally { if (tempContext != null) - tempContext.Dispose(); + tempContext.Dispose(); // nulls the ThreadStatic context } - } /// diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index b119c9a284..73b28ae669 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -52,16 +52,18 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } + UmbracoContext tempContext = null; try { // DO not run publishing if content is re-loading if (content.Instance.isInitializing == false) { //TODO: We should remove this in v8, this is a backwards compat hack + // see notes in CacheRefresherEventHandler // because notifications will not be sent if there is no UmbracoContext // see NotificationServiceExtensions var httpContext = new HttpContextWrapper(new HttpContext(new SimpleWorkerRequest("temp.aspx", "", new StringWriter()))); - UmbracoContext.EnsureContext( + tempContext = UmbracoContext.EnsureContext( httpContext, _appContext, new WebSecurity(httpContext, _appContext), @@ -78,6 +80,11 @@ namespace Umbraco.Web.Scheduling { Logger.Error("Failed (see exception).", e); } + finally + { + if (tempContext != null) + tempContext.Dispose(); // nulls the ThreadStatic context + } return true; // repeat } From 2bff1bfbb6e1fc2673e5a7388eaa70dbc215f487 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 13 Jul 2017 14:20:29 +0200 Subject: [PATCH 388/714] U4-10121 - properly dispose context --- src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 3ebfe25ff8..e802a9c0fa 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -797,9 +797,6 @@ namespace Umbraco.Web.Cache //TODO: We should remove this in v8, this is a backwards compat hack and is needed because when we are using Deploy, the events will be raised on a background //thread which means that cache refreshers will also execute on a background thread and in many cases developers may be using UmbracoContext.Current in their //cache refresher handlers, so before we execute all of the events, we'll ensure a context - // - // note that this means we must *exclusively* use this method in a background task else we'd replace and dispose a real context - // UmbracoContext tempContext = null; if (UmbracoContext.Current == null) { From 9fbf1aedac9f6935c42025b838bdce9359368ab2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 14 Jul 2017 10:43:12 +1000 Subject: [PATCH 389/714] adds both post and delete verbs --- src/Umbraco.Web/Editors/UserGroupsController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index 9a1e26007f..7c2b589e97 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -87,6 +87,8 @@ namespace Umbraco.Web.Editors return display; } + [HttpPost] + [HttpDelete] public HttpResponseMessage PostDeleteUserGroups([FromUri] int[] userGroupIds) { var userGroups = Services.UserService.GetAllUserGroups(userGroupIds).ToArray(); From db863a45923ec6dadc95021206851e376a009a1d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 14 Jul 2017 10:49:10 +1000 Subject: [PATCH 390/714] update sln to stop building the damn UI project --- src/umbraco.sln | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/umbraco.sln b/src/umbraco.sln index df260903ee..a2603aecbe 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.26430.15 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}" EndProject @@ -127,9 +127,7 @@ Global {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.Build.0 = Release|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Release|Any CPU.ActiveCfg = Release|Any CPU From 29aaa2602258557fa8a38f116194245df6d0309e Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 14 Jul 2017 20:13:57 +0200 Subject: [PATCH 391/714] U4-10023 - fix media picker for multiple start nodes --- .../Persistence/Querying/IQuery.cs | 8 ++- .../Persistence/Querying/Query.cs | 45 +++++++++++++ src/Umbraco.Core/Services/EntityService.cs | 37 ++++++++++ src/Umbraco.Core/Services/IEntityService.cs | 6 ++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + src/Umbraco.Web/Editors/EntityController.cs | 67 +++++++++++++++++-- src/Umbraco.Web/Editors/MediaController.cs | 26 +++++++ 7 files changed, 185 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Querying/IQuery.cs b/src/Umbraco.Core/Persistence/Querying/IQuery.cs index ae986baddc..cd08274c1b 100644 --- a/src/Umbraco.Core/Persistence/Querying/IQuery.cs +++ b/src/Umbraco.Core/Persistence/Querying/IQuery.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; namespace Umbraco.Core.Persistence.Querying @@ -16,6 +17,11 @@ namespace Umbraco.Core.Persistence.Querying /// This instance so calls to this method are chainable IQuery Where(Expression> predicate); - + /// + /// Adds a set of OR-ed where clauses to the query. + /// + /// + /// This instance so calls to this method are chainable. + IQuery WhereAny(IEnumerable>> predicates); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/Query.cs b/src/Umbraco.Core/Persistence/Querying/Query.cs index 6213ca5ed6..d49296c6d0 100644 --- a/src/Umbraco.Core/Persistence/Querying/Query.cs +++ b/src/Umbraco.Core/Persistence/Querying/Query.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Text; namespace Umbraco.Core.Persistence.Querying { @@ -39,6 +40,50 @@ namespace Umbraco.Core.Persistence.Querying return this; } + /// + /// Adds a set of OR-ed where clauses to the query. + /// + /// + /// This instance so calls to this method are chainable. + public virtual IQuery WhereAny(IEnumerable>> predicates) + { + if (predicates == null) return this; + + StringBuilder sb = null; + List parameters = null; + Sql sql = null; + foreach (var predicate in predicates) + { + // see notes in Where() + var expressionHelper = new ModelToSqlExpressionVisitor(); + var whereExpression = expressionHelper.Visit(predicate); + + if (sb == null) + { + sb = new StringBuilder("("); + parameters = new List(); + sql = new Sql(); + } + else + { + sb.Append(" OR "); + sql.Append(" OR "); + } + + sb.Append(whereExpression); + parameters.AddRange(expressionHelper.GetSqlParameters()); + sql.Append(whereExpression, expressionHelper.GetSqlParameters()); + } + + if (sb == null) return this; + + sb.Append(")"); + //_wheres.Add(Tuple.Create(sb.ToString(), parameters.ToArray())); + _wheres.Add(Tuple.Create("(" + sql.SQL + ")", sql.Arguments)); + + return this; + } + /// /// Returns all translated where clauses and their sql parameters /// diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 04d6c276b2..ecf1b5a216 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; using Umbraco.Core.Cache; using Umbraco.Core.CodeAnnotations; using Umbraco.Core.Events; @@ -361,6 +364,40 @@ namespace Umbraco.Core.Services return contents; } } + /// + /// Returns a paged collection of descendants. + /// + public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateEntityRepository(uow); + + var query = Query.Builder; + var idsA = ids.ToArray(); + if (idsA.All(x => x != Constants.System.Root)) + { + var clauses = new List>>(); + foreach (var id in idsA) + { + var qid = id; + clauses.Add(x => x.Path.SqlContains(string.Format(",{0},", qid), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + } + query.WhereAny(clauses); + } + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); + } + + var contents = repository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } /// /// Returns a paged collection of descendants from the root diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 82e5227cf2..f6c75c2e98 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -175,6 +175,12 @@ namespace Umbraco.Core.Services IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Returns a paged collection of descendants + /// + IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// /// Returns a paged collection of descendants from the root /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8a635b966c..109b11f156 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -118,6 +118,7 @@ + diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 37d712c362..a2aa8e6b4d 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -19,6 +19,7 @@ using Umbraco.Web.Dynamics; using System.Text.RegularExpressions; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using System.Web.Http.Controllers; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Xml; namespace Umbraco.Web.Editors @@ -541,11 +542,30 @@ namespace Umbraco.Web.Editors var objectType = ConvertToObjectType(type); if (objectType.HasValue) { + IEnumerable entities; long totalRecords; - //if it's from root, don't return recycled - var entities = id == Constants.System.Root - ? Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed:false) - : Services.EntityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); + + if (id == Constants.System.Root) + { + int[] aids = null; + switch (type) + { + case UmbracoEntityTypes.Document: + aids = Security.CurrentUser.AllStartContentIds; + break; + case UmbracoEntityTypes.Media: + aids = Security.CurrentUser.AllStartMediaIds; + break; + } + + entities = aids != null && aids.Length > 0 + ? Services.EntityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter) + : Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed: false); + } + else + { + entities = Services.EntityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); + } if (totalRecords == 0) { @@ -618,9 +638,17 @@ namespace Umbraco.Web.Editors { if (startNode > 0) { + // descendants + // "__Path: -1*,1234,*" -- the first "*" stands for path-to-1234 sb.Append("__Path: \\-1*\\,"); sb.Append(startNode.ToString(CultureInfo.InvariantCulture)); sb.Append("\\,* "); + + // self + // "__Path: -1*,1234" -- the first "*" stands for path-to-1234 + sb.Append("__Path: \\-1*\\,"); + sb.Append(startNode.ToString(CultureInfo.InvariantCulture)); + sb.Append(" "); } } if (startNodes.Length > 0) @@ -918,6 +946,37 @@ namespace Umbraco.Web.Editors var ids = Services.EntityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray(); + int[] aids = null; + switch (entityType) + { + case UmbracoEntityTypes.Document: + aids = Security.CurrentUser.AllStartContentIds; + break; + case UmbracoEntityTypes.Media: + aids = Security.CurrentUser.AllStartMediaIds; + break; + } + + if (aids != null && aids.Length > 0) + { + var lids = new List(); + var ok = false; + foreach (var i in ids) + { + if (ok) + { + lids.Add(i); + continue; + } + if (aids.Contains(i)) + { + lids.Add(i); + ok = true; + } + } + ids = lids.ToArray(); + } + return ids.Length == 0 ? Enumerable.Empty() : Services.EntityService.GetAll(objectType.Value, ids) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index ed4b1c2f63..1603615a58 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -250,6 +250,13 @@ namespace Umbraco.Web.Editors } #region GetChildren + + private int[] _userStartNodes; + protected int[] UserStartNodes + { + get { return _userStartNodes ?? (_userStartNodes = Security.CurrentUser.AllStartMediaIds); } + } + /// /// Returns the child media objects - using the entity INT id /// @@ -262,6 +269,25 @@ namespace Umbraco.Web.Editors bool orderBySystemField = true, string filter = "") { + //if a request is made for the root node data but the user's start node is not the default, then + // we need to return their start nodes + if (id == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + { + if (pageNumber > 0) + return new PagedResult>(0, 0, 0); + var nodes = Services.MediaService.GetByIds(UserStartNodes).ToArray(); + if (nodes.Length == 0) + return new PagedResult>(0, 0, 0); + if (pageSize < nodes.Length) pageSize = nodes.Length; // bah + var pr = new PagedResult>(nodes.Length, pageNumber, pageSize) + { + Items = nodes.Select(Mapper.Map>) + }; + return pr; + } + + // else proceed as usual + long totalChildren; IMedia[] children; if (pageNumber > 0 && pageSize > 0) From e13529198032985c80f561a19b566e0d6a02ae21 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 17 Jul 2017 18:23:20 +1000 Subject: [PATCH 392/714] Fixes issue with user group start nodes, they are allowed to have null and by default this will be the case but we couldn't remove them and the ROOT node wasn't being mapped. --- .../services/umbdataformatter.service.js | 11 ++---- .../Models/ContentEditing/UserGroupSave.cs | 4 +-- .../Models/Mapping/UserModelMapper.cs | 36 ++++++++++++++++--- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index d858a5788f..2f29a1677b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -180,7 +180,7 @@ } saveModel.users = formattedUsers; - //make sure the startnodes are just an int + //make sure the startnodes are just an int if one is set var props = ["startContentId", "startMediaId"]; for (var m = 0; m < props.length; m++) { var startId = saveModel[props[m]]; @@ -190,14 +190,7 @@ saveModel[props[m]] = startId.id; } - saveModel.parentId = -1; - if (!saveModel.startContentId) { - saveModel.startContentId = -1; - } - if (!saveModel.startMediaId) { - saveModel.startMediaId = -1; - } - + saveModel.parentId = -1; return saveModel; }, diff --git a/src/Umbraco.Web/Models/ContentEditing/UserGroupSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserGroupSave.cs index 05db0429cb..872834988d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserGroupSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserGroupSave.cs @@ -33,10 +33,10 @@ namespace Umbraco.Web.Models.ContentEditing public IEnumerable Users { get; set; } [DataMember(Name = "startContentId")] - public int StartContentId { get; set; } + public int? StartContentId { get; set; } [DataMember(Name = "startMediaId")] - public int StartMediaId { get; set; } + public int? StartMediaId { get; set; } /// /// The list of letters (permission codes) to assign as the default for the user group diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 8056724a68..bb9f212140 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Models.Mapping .ConstructUsing((UserGroupSave save) => new UserGroup() { CreateDate = DateTime.Now }) .IgnoreDeletableEntityCommonProperties() .ForMember(dest => dest.Id, map => map.Condition(source => GetIntId(source.Id) > 0)) - .ForMember(dest => dest.Id, map => map.MapFrom(source => GetIntId(source.Id))) + .ForMember(dest => dest.Id, map => map.MapFrom(source => GetIntId(source.Id))) .ForMember(dest => dest.Permissions, map => map.MapFrom(source => source.DefaultPermissions)) .AfterMap((save, userGroup) => { @@ -296,8 +296,8 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(applicationContext.Services.UserService, applicationContext.ApplicationCache.RuntimeCache))) .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => GetIntId(user.Id))) - .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds)) - .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds)) + .ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.AllStartContentIds)) + .ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.AllStartMediaIds)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember( detail => detail.EmailHash, @@ -313,8 +313,8 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Groups.ToArray())) - .ForMember(detail => detail.StartContentNodes, opt => opt.MapFrom(user => user.StartContentIds)) - .ForMember(detail => detail.StartMediaNodes, opt => opt.MapFrom(user => user.StartMediaIds)) + .ForMember(detail => detail.StartContentNodes, opt => opt.MapFrom(user => user.AllStartContentIds)) + .ForMember(detail => detail.StartMediaNodes, opt => opt.MapFrom(user => user.AllStartMediaIds)) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember(detail => detail.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp)); @@ -325,22 +325,48 @@ namespace Umbraco.Web.Models.Mapping { var allSections = services.SectionService.GetSections(); display.Sections = allSections.Where(x => Enumerable.Contains(group.AllowedSections, x.Alias)).Select(Mapper.Map); + if (group.StartMediaId > 0) { display.MediaStartNode = Mapper.Map( services.EntityService.Get(group.StartMediaId, UmbracoObjectTypes.Media)); } + else if (group.StartMediaId == -1) + { + //create the root node + display.MediaStartNode = RootNode(); + } + if (group.StartContentId > 0) { display.ContentStartNode = Mapper.Map( services.EntityService.Get(group.StartContentId, UmbracoObjectTypes.Document)); } + else if (group.StartContentId == -1) + { + //create the root node + display.ContentStartNode = RootNode(); + } + if (display.Icon.IsNullOrWhiteSpace()) { display.Icon = "icon-users"; } } + private EntityBasic RootNode() + { + return new EntityBasic + { + Name = "ROOT", + Path = "-1", + Icon = "icon-document", + Id = -1, + Trashed = false, + ParentId = -1 + }; + } + private static int GetIntId(object id) { var result = id.TryConvertTo(); From d1d74203eaaf3149c1e2b8a4530d991932197afb Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 17 Jul 2017 19:40:45 +1000 Subject: [PATCH 393/714] Ability to select root node in user group picker so the user group can explicitly allow root access or not. --- .../src/common/services/mediatypehelper.service.js | 4 ++++ .../overlays/treepicker/treepicker.controller.js | 6 ++++-- .../views/components/users/umb-user-group-preview.html | 8 ++++---- .../src/views/users/group.controller.js | 9 +++++++++ src/Umbraco.Web/Models/Mapping/UserModelMapper.cs | 10 +++++----- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js index 87804c8473..7e0e1db24f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js @@ -23,6 +23,10 @@ function mediaTypeHelper(mediaTypeResource, $q) { getAllowedImagetypes: function (mediaId){ //TODO: This is horribly inneficient - why make one request per type!? + //This should make a call to c# to get exactly what it's looking for instead of returning every single media type and doing + //some filtering on the client side. + //This is also called multiple times when it's not needed! Example, when launching the media picker, this will be called twice + //which means we'll be making at least 6 REST calls to fetch each media type // Get All allowedTypes return mediaTypeResource.getAllowedTypes(mediaId) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index 7ba8970294..a552c18de2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -9,7 +9,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.section = dialogOptions.section; $scope.treeAlias = dialogOptions.treeAlias; $scope.multiPicker = dialogOptions.multiPicker; - $scope.hideHeader = true; + $scope.hideHeader = (typeof dialogOptions.hideHeader) === "boolean" ? dialogOptions.hideHeader : false; // if you need to load a not initialized tree set this value to false - default is true $scope.onlyInitialized = dialogOptions.onlyInitialized; $scope.searchInfo = { @@ -28,7 +28,9 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.emptyStateMessage = dialogOptions.emptyStateMessage; - //TODO: I don't think this is used or called anywhere!! + //This is called from ng-init + //it turns out it is called from the angular html : / Have a look at views/common / overlays / contentpicker / contentpicker.html you'll see ng-init. + //this is probably an anti pattern IMO and shouldn't be used $scope.init = function (contentType) { if (contentType === "content") { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html index e4a0371ee3..7d5238d3a6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html @@ -16,16 +16,16 @@
Content start node: - {{ contentStartNode.name }} - Content root + {{ contentStartNode.name }} + Content root
Media start node: - {{ mediaStartNode.name }} - Media root + {{ mediaStartNode.name }} + Media root
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js index 9aca057ee1..09b61c4605 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js @@ -105,10 +105,15 @@ title: "Select content start node", view: "contentpicker", hideSubmitButton: true, + hideHeader: false, show: true, submit: function (model) { if (model.selection) { vm.userGroup.contentStartNode = model.selection[0]; + if (vm.userGroup.contentStartNode.id === "-1") { + vm.userGroup.contentStartNode.name = "Content Root"; + vm.userGroup.contentStartNode.icon = "icon-document"; + } } vm.contentPicker.show = false; vm.contentPicker = null; @@ -132,6 +137,10 @@ submit: function (model) { if (model.selection) { vm.userGroup.mediaStartNode = model.selection[0]; + if (vm.userGroup.mediaStartNode.id === "-1") { + vm.userGroup.mediaStartNode.name = "Media Root"; + vm.userGroup.mediaStartNode.icon = "icon-picture"; + } } vm.contentPicker.show = false; vm.contentPicker = null; diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index bb9f212140..36e7c33d7b 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -334,7 +334,7 @@ namespace Umbraco.Web.Models.Mapping else if (group.StartMediaId == -1) { //create the root node - display.MediaStartNode = RootNode(); + display.MediaStartNode = RootNode("Media Root", "icon-picture"); } if (group.StartContentId > 0) @@ -345,7 +345,7 @@ namespace Umbraco.Web.Models.Mapping else if (group.StartContentId == -1) { //create the root node - display.ContentStartNode = RootNode(); + display.ContentStartNode = RootNode("Content Root", "icon-document"); } if (display.Icon.IsNullOrWhiteSpace()) @@ -354,13 +354,13 @@ namespace Umbraco.Web.Models.Mapping } } - private EntityBasic RootNode() + private EntityBasic RootNode(string name, string icon) { return new EntityBasic { - Name = "ROOT", + Name = name, Path = "-1", - Icon = "icon-document", + Icon = icon, Id = -1, Trashed = false, ParentId = -1 From d99f7b6ff69b9db4c6350854f3135d51ba3ed198 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 17 Jul 2017 19:51:51 +1000 Subject: [PATCH 394/714] Ability to select the root node for the users start node --- .../treepicker/treepicker.controller.js | 25 ++++++++----------- .../src/views/users/group.controller.js | 5 ++-- .../src/views/users/user.controller.js | 14 +++++++++-- .../Models/Mapping/UserModelMapper.cs | 8 +++--- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index a552c18de2..b7512e1e8c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -9,7 +9,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.section = dialogOptions.section; $scope.treeAlias = dialogOptions.treeAlias; $scope.multiPicker = dialogOptions.multiPicker; - $scope.hideHeader = (typeof dialogOptions.hideHeader) === "boolean" ? dialogOptions.hideHeader : false; + $scope.hideHeader = (typeof dialogOptions.hideHeader) === "boolean" ? dialogOptions.hideHeader : true; // if you need to load a not initialized tree set this value to false - default is true $scope.onlyInitialized = dialogOptions.onlyInitialized; $scope.searchInfo = { @@ -209,26 +209,23 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", function select(text, id, entity) { //if we get the root, we just return a constructed entity, no need for server data if (id < 0) { - if ($scope.multiPicker) { + var rootNode = { + alias: null, + icon: "icon-folder", + id: id, + name: text + }; + + if ($scope.multiPicker) { if (entity) { multiSelectItem(entity); } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - multiSelectItem(ent); - }); + multiSelectItem(rootNode); } - } else { - var node = { - alias: null, - icon: "icon-folder", - id: id, - name: text - }; - $scope.model.selection.push(node); + $scope.model.selection.push(rootNode); $scope.model.submit($scope.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js index 09b61c4605..659a1f0c47 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js @@ -112,7 +112,7 @@ vm.userGroup.contentStartNode = model.selection[0]; if (vm.userGroup.contentStartNode.id === "-1") { vm.userGroup.contentStartNode.name = "Content Root"; - vm.userGroup.contentStartNode.icon = "icon-document"; + vm.userGroup.contentStartNode.icon = "icon-folder"; } } vm.contentPicker.show = false; @@ -133,13 +133,14 @@ treeAlias: "media", entityType: "media", hideSubmitButton: true, + hideHeader: false, show: true, submit: function (model) { if (model.selection) { vm.userGroup.mediaStartNode = model.selection[0]; if (vm.userGroup.mediaStartNode.id === "-1") { vm.userGroup.mediaStartNode.name = "Media Root"; - vm.userGroup.mediaStartNode.icon = "icon-picture"; + vm.userGroup.mediaStartNode.icon = "icon-folder"; } } vm.contentPicker.show = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 07539b79d2..e0a3f08b4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -131,11 +131,16 @@ view: "contentpicker", multiPicker: true, selection: vm.user.startContentIds, + hideHeader: false, show: true, submit: function (model) { // select items if (model.selection) { - angular.forEach(model.selection, function(item){ + angular.forEach(model.selection, function (item) { + if (item.id === "-1") { + item.name = "Content Root"; + item.icon = "icon-folder"; + } multiSelectItem(item, vm.user.startContentIds); }); } @@ -159,11 +164,16 @@ treeAlias: "media", entityType: "media", multiPicker: true, + hideHeader: false, show: true, submit: function (model) { // select items if (model.selection) { - angular.forEach(model.selection, function(item){ + angular.forEach(model.selection, function (item) { + if (item.id === "-1") { + item.name = "Media Root"; + item.icon = "icon-folder"; + } multiSelectItem(item, vm.user.startMediaIds); }); } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 36e7c33d7b..aab4a30a4e 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -334,7 +334,7 @@ namespace Umbraco.Web.Models.Mapping else if (group.StartMediaId == -1) { //create the root node - display.MediaStartNode = RootNode("Media Root", "icon-picture"); + display.MediaStartNode = RootNode("Media Root"); } if (group.StartContentId > 0) @@ -345,7 +345,7 @@ namespace Umbraco.Web.Models.Mapping else if (group.StartContentId == -1) { //create the root node - display.ContentStartNode = RootNode("Content Root", "icon-document"); + display.ContentStartNode = RootNode("Content Root"); } if (display.Icon.IsNullOrWhiteSpace()) @@ -354,13 +354,13 @@ namespace Umbraco.Web.Models.Mapping } } - private EntityBasic RootNode(string name, string icon) + private EntityBasic RootNode(string name) { return new EntityBasic { Name = name, Path = "-1", - Icon = icon, + Icon = "icon-folder", Id = -1, Trashed = false, ParentId = -1 From 193682cd4c77a4f1681ba8947e5a23f06ed28d76 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 17 Jul 2017 20:09:00 +1000 Subject: [PATCH 395/714] Ability to select the root node for the users start node --- .../services/umbdataformatter.service.js | 2 +- .../Models/Mapping/UserModelMapper.cs | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 2f29a1677b..75c7b02d55 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -105,7 +105,7 @@ } var formattedIds = []; for (var j = 0; j < startIds.length; j++) { - formattedIds.push(startIds[j].id); + formattedIds.push(Number(startIds[j].id)); } saveModel[props[m]] = formattedIds; } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index aab4a30a4e..f925df4829 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -254,17 +254,30 @@ namespace Umbraco.Web.Models.Mapping var startContentIds = user.StartContentIds.ToArray(); if (startContentIds.Length > 0) - { + { //TODO: Update GetAll to be able to pass in a parameter like on the normal Get to NOT load in the entire object! - + var startNodes = new List(); + if (startContentIds.Contains(-1)) + { + startNodes.Add(RootNode("Content Root")); + } var contentItems = applicationContext.Services.EntityService.GetAll(UmbracoObjectTypes.Document, startContentIds); - display.StartContentIds = Mapper.Map, IEnumerable>(contentItems); + startNodes.AddRange(Mapper.Map, IEnumerable>(contentItems)); + display.StartContentIds = startNodes; + + } var startMediaIds = user.StartMediaIds.ToArray(); if (startMediaIds.Length > 0) { + var startNodes = new List(); + if (startContentIds.Contains(-1)) + { + startNodes.Add(RootNode("Media Root")); + } var mediaItems = applicationContext.Services.EntityService.GetAll(UmbracoObjectTypes.Media, startMediaIds); - display.StartMediaIds = Mapper.Map, IEnumerable>(mediaItems); + startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); + display.StartMediaIds = startNodes; } }); From 3ac64f18d24f51c6e50f4638f989a6967a8be520 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 17 Jul 2017 15:08:45 +0200 Subject: [PATCH 396/714] Bugfix UserModelMapper --- .../Persistence/Factories/UserGroupFactory.cs | 2 +- .../Models/Mapping/UserModelMapperTests.cs | 47 +++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Tests/Models/Mapping/UserModelMapperTests.cs diff --git a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs index 20057db20f..f4ecffca54 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Persistence.Factories var userGroup = new UserGroup(dto.UserCount, dto.Alias, dto.Name, dto.DefaultPermissions.IsNullOrWhiteSpace() ? Enumerable.Empty() - : dto.DefaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)), + : dto.DefaultPermissions.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(), dto.Icon); try diff --git a/src/Umbraco.Tests/Models/Mapping/UserModelMapperTests.cs b/src/Umbraco.Tests/Models/Mapping/UserModelMapperTests.cs new file mode 100644 index 0000000000..7f5e975cfc --- /dev/null +++ b/src/Umbraco.Tests/Models/Mapping/UserModelMapperTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMapper; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models.Membership; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; + +namespace Umbraco.Tests.Models.Mapping +{ + [TestFixture] + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + public class UserModelMapperTests : BaseDatabaseFactoryTest + { + [Test] + public void Map_UserGroupSave_To_IUserGroup() + { + var userModelMapper = new UserModelMapper(); + Mapper.Initialize(configuration => userModelMapper.ConfigureMappings(configuration, ApplicationContext.Current)); + + var userService = ApplicationContext.Services.UserService; + IUserGroup userGroup = new UserGroup(0, "alias", "name", new List { "c" }, "icon"); + userService.Save(userGroup); + + // userGroup.permissions is System.Collections.Generic.List`1[System.String] + + userGroup = userService.GetUserGroupById(userGroup.Id); + + // userGroup.permissions is System.Linq.Enumerable+WhereSelectArrayIterator`2[System.Char, System.String] + // fixed: now System.Collections.Generic.List`1[System.String] + + const string json = "{\"id\":@@@ID@@@,\"alias\":\"perm1\",\"name\":\"Perm1\",\"icon\":\"icon-users\",\"sections\":[\"content\"],\"users\":[],\"defaultPermissions\":[\"F\",\"C\",\"A\"],\"assignedPermissions\":{},\"startContentId\":-1,\"startMediaId\":-1,\"action\":\"save\",\"parentId\":-1}"; + var userGroupSave = JsonConvert.DeserializeObject(json.Replace("@@@ID@@@", userGroup.Id.ToString())); + + // failed, AutoMapper complained, "Unable to cast object of type 'WhereSelectArrayIterator`2[System.Char,System.String]' to type 'System.Collections.IList'". + // fixmed: added ToList() in UserGroupFactory + Mapper.Map(userGroupSave, userGroup); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 5a9a79db47..3d4a8d7320 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -186,6 +186,7 @@ + From a0d3c18acc8cdaaa2772d2f7b92c8ab97da6c807 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 18 Jul 2017 13:10:17 +1000 Subject: [PATCH 397/714] Fixes build script to work with VS 2017 (only) and fixes BuildBelle since it didn't work and is missing tools --- build/Build.bat | 10 ---------- build/BuildBelle.bat | 23 ++++++++++++++++------- src/umbraco.presentation.targets | 4 ++++ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/build/Build.bat b/build/Build.bat index f26c4877cd..da34691c3d 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -125,22 +125,12 @@ IF NOT EXIST "%nuGetExecutable%" ( powershell -Command "(New-Object Net.WebClient).DownloadFile('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', '%nuGetExecutable%')" ) -:: We need 7za.exe for BuildBelle.bat -IF NOT EXIST "%toolsFolder%7za.exe" ( - ECHO 7zip not found - fetching now - "%nuGetExecutable%" install 7-Zip.CommandLine -OutputDirectory tools -Verbosity quiet -) - :: We need vswhere.exe for VS2017+ IF NOT EXIST "%toolsFolder%vswhere.exe" ( ECHO vswhere not found - fetching now "%nuGetExecutable%" install vswhere -OutputDirectory tools -Verbosity quiet ) -:: Put 7za.exe and vswhere.exe in a predictable path (not version specific) -FOR /f "delims=" %%A in ('dir "%toolsFolder%7-Zip.CommandLine.*" /b') DO SET "sevenZipExePath=%toolsFolder%%%A\" -MOVE "%sevenZipExePath%tools\7za.exe" "%toolsFolder%7za.exe" - FOR /f "delims=" %%A in ('dir "%toolsFolder%vswhere.*" /b') DO SET "vswhereExePath=%toolsFolder%%%A\" MOVE "%vswhereExePath%tools\vswhere.exe" "%toolsFolder%vswhere.exe" diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 78b7736c2b..750ffae6b2 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -8,6 +8,22 @@ ECHO Current folder: %CD% SET nodeFileName=node-v6.9.1-win-x86.7z SET nodeExtractFolder=%toolsFolder%node.js.691 +SET nuGetExecutable=%CD%\tools\nuget.exe +IF NOT EXIST "%nuGetExecutable%" ( + ECHO Downloading https://dist.nuget.org/win-x86-commandline/latest/nuget.exe to %nuGetExecutable% + powershell -Command "(New-Object Net.WebClient).DownloadFile('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', '%nuGetExecutable%')" +) + +:: We need 7za.exe for BuildBelle.bat +IF NOT EXIST "%toolsFolder%7za.exe" ( + ECHO 7zip not found - fetching now + "%nuGetExecutable%" install 7-Zip.CommandLine -OutputDirectory tools -Verbosity quiet +) + +:: Put 7za.exe and vswhere.exe in a predictable path (not version specific) +FOR /f "delims=" %%A in ('dir "%toolsFolder%7-Zip.CommandLine.*" /b') DO SET "sevenZipExePath=%toolsFolder%%%A\" +MOVE "%sevenZipExePath%tools\7za.exe" "%toolsFolder%7za.exe" + IF NOT EXIST "%nodeExtractFolder%" ( ECHO Downloading http://nodejs.org/dist/v6.9.1/%nodeFileName% to %toolsFolder%%nodeFileName% powershell -Command "(New-Object Net.WebClient).DownloadFile('http://nodejs.org/dist/v6.9.1/%nodeFileName%', '%toolsFolder%%nodeFileName%')" @@ -16,13 +32,6 @@ IF NOT EXIST "%nodeExtractFolder%" ( ) FOR /f "delims=" %%A in ('dir "%nodeExtractFolder%\node*" /b') DO SET "nodePath=%nodeExtractFolder%\%%A" - -SET nuGetExecutable=%CD%\tools\nuget.exe -IF NOT EXIST "%nuGetExecutable%" ( - ECHO Downloading https://dist.nuget.org/win-x86-commandline/latest/nuget.exe to %nuGetExecutable% - powershell -Command "(New-Object Net.WebClient).DownloadFile('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', '%nuGetExecutable%')" -) - SET drive=%CD:~0,2% SET nuGetFolder=%drive%\packages\ FOR /f "delims=" %%A in ('dir "%nuGetFolder%npm.*" /b') DO SET "npmPath=%nuGetFolder%%%A\" diff --git a/src/umbraco.presentation.targets b/src/umbraco.presentation.targets index 2a33705d6f..6eb87ad9fa 100644 --- a/src/umbraco.presentation.targets +++ b/src/umbraco.presentation.targets @@ -58,6 +58,10 @@ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll + + + + $(VSWherePath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index ffd5a9b0fd..17fb9eeb19 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -41,7 +41,7 @@ namespace Umbraco.Web.Editors var membershipPasswordHasher = userMgr.PasswordHasher as IMembershipProviderPasswordHasher; //check if this identity implementation is powered by an IUserAwarePasswordHasher (it will be by default in 7.7+ but not for upgrades) - var userAwarePasswordHasher = userMgr.PasswordHasher as IUserAwarePasswordHasher; + var userAwarePasswordHasher = userMgr.PasswordHasher as IUserAwarePasswordHasher; if (membershipPasswordHasher != null && userAwarePasswordHasher == null) { @@ -49,22 +49,22 @@ namespace Umbraco.Web.Editors if (membershipPasswordHasher.MembershipProvider.RequiresQuestionAndAnswer) throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified"); return ChangePasswordWithMembershipProvider(currentUser.Username, passwordModel, membershipPasswordHasher.MembershipProvider); - } + } - //get the real password validator, thsi should not be null but in some very rare cases it could be, in which case - //we need to create a default password validator to use since we have no idea what it actually is or what it's rules are - //this is an Edge Case! - var passwordValidator = userMgr.PasswordValidator as PasswordValidator - ?? (membershipPasswordHasher != null - ? new MembershipProviderPasswordValidator(membershipPasswordHasher.MembershipProvider) - : new PasswordValidator()); + //if we are here, then a IUserAwarePasswordHasher is available, however we cannot proceed in that case if for some odd reason + //the user has configured the membership provider to not be hashed. This will actually never occur because the BackOfficeUserManager + //will throw if it's not hashed, but we should make sure to check anyways (i.e. in case we want to unit test!) + if (membershipPasswordHasher != null && membershipPasswordHasher.MembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed) + { + throw new InvalidOperationException("The membership provider cannot have a password format of " + membershipPasswordHasher.MembershipProvider.PasswordFormat + " and be configured with secured hashed passwords"); + } //Are we resetting the password?? if (passwordModel.Reset.HasValue && passwordModel.Reset.Value) { //ok, we should be able to reset it var resetToken = await userMgr.GeneratePasswordResetTokenAsync(currentUser.Id); - var newPass = Membership.GeneratePassword(passwordValidator.RequiredLength, passwordValidator.RequireNonLetterOrDigit ? 2 : 0); + var newPass = userMgr.GeneratePassword(); var resetResult = await userMgr.ResetPasswordAsync(currentUser.Id, resetToken, newPass); if (resetResult.Succeeded == false) From b58799eb3d93af6e9f22f5522c43cbe0c8cdfa6d Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 20 Jul 2017 13:58:42 +1000 Subject: [PATCH 415/714] Gets everything implemented and it all works - still uses the old hashing mechanism but everything is ready to be plugged in to support any new types of hashing. --- .../Security/BackOfficeUserManager.cs | 21 ++++++++- .../IMembershipProviderPasswordHasher.cs | 12 ++++++ .../Security/IUserAwarePasswordHasher.cs | 18 ++++++++ .../MembershipProviderPasswordHasher.cs | 43 ------------------- ...erAwareMembershipProviderPasswordHasher.cs | 30 +++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 3 ++ 6 files changed, 82 insertions(+), 45 deletions(-) create mode 100644 src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs create mode 100644 src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs create mode 100644 src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index b1c4b23bbb..ef712623a5 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -302,8 +302,25 @@ namespace Umbraco.Core.Security public override Task ChangePasswordAsync(int userId, string currentPassword, string newPassword) { return base.ChangePasswordAsync(userId, currentPassword, newPassword); - } - + } + + /// + /// Override to determine how to hash the password + /// + /// + /// + /// + /// + protected override async Task VerifyPasswordAsync(IUserPasswordStore store, T user, string password) + { + var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher; + if (userAwarePasswordHasher == null) + return await base.VerifyPasswordAsync(store, user, password); + + var hash = await store.GetPasswordHashAsync(user); + return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed; + } + /// /// Override to determine how to hash the password /// diff --git a/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs new file mode 100644 index 0000000000..42715d280a --- /dev/null +++ b/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A password hasher that is based on the rules configured for a membership provider + /// + public interface IMembershipProviderPasswordHasher : IPasswordHasher + { + MembershipProviderBase MembershipProvider { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs b/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs new file mode 100644 index 0000000000..cea4d5a144 --- /dev/null +++ b/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A password hasher that is User aware so that it can process the hashing based on the user's settings + /// + /// + /// + public interface IUserAwarePasswordHasher + where TUser : class, IUser + where TKey : IEquatable + { + string HashPassword(TUser user, string password); + PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs index 5697ab68fc..f518f99c55 100644 --- a/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs +++ b/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs @@ -1,50 +1,7 @@ -using System; using Microsoft.AspNet.Identity; -using Umbraco.Core.Models.Identity; namespace Umbraco.Core.Security { - /// - /// A password hasher that is User aware so that it can process the hashing based on the user's settings - /// - /// - /// - public interface IUserAwarePasswordHasher - where TUser : class, IUser - where TKey : IEquatable - { - string HashPassword(TUser user, string password); - string VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword); - } - - /// - /// A password hasher that is based on the rules configured for a membership provider - /// - public interface IMembershipProviderPasswordHasher : IPasswordHasher - { - MembershipProviderBase MembershipProvider { get; } - } - - /// - /// The default password hasher that is User aware so that it can process the hashing based on the user's settings - /// - public class UserAwareMembershipProviderPasswordHasher : MembershipProviderPasswordHasher, IUserAwarePasswordHasher - { - public UserAwareMembershipProviderPasswordHasher(MembershipProviderBase provider) : base(provider) - { - } - - public string HashPassword(BackOfficeIdentityUser user, string password) - { - throw new NotImplementedException(); - } - - public string VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword) - { - throw new NotImplementedException(); - } - } - /// /// A password hasher that conforms to the password hashing done with membership providers /// diff --git a/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs new file mode 100644 index 0000000000..7d19d72c3c --- /dev/null +++ b/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// The default password hasher that is User aware so that it can process the hashing based on the user's settings + /// + public class UserAwareMembershipProviderPasswordHasher : MembershipProviderPasswordHasher, IUserAwarePasswordHasher + { + public UserAwareMembershipProviderPasswordHasher(MembershipProviderBase provider) : base(provider) + { + } + + public string HashPassword(BackOfficeIdentityUser user, string password) + { + //TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 + //NOTE: For now this just falls back to the hashing we are currently using + return base.HashPassword(password); + } + + public PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword) + { + //TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 + //NOTE: For now this just falls back to the hashing we are currently using + return base.VerifyHashedPassword(hashedPassword, providedPassword); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4d46e6c319..2a43836b16 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -663,10 +663,13 @@ + + + From 540c3389671af76aa277098d4c1715ccb39a7a3f Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 20 Jul 2017 14:09:51 +1000 Subject: [PATCH 416/714] ensure the password config values are saved when the user is created/updated --- .../Repositories/UserRepository.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index cb9c471103..03de3dba41 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -4,6 +4,8 @@ using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using System.Text; +using System.Web.Security; +using Newtonsoft.Json; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; @@ -15,6 +17,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Relators; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Security; namespace Umbraco.Core.Persistence.Repositories { @@ -223,9 +226,18 @@ ORDER BY colName"; if (entity.SecurityStamp.IsNullOrWhiteSpace()) { entity.SecurityStamp = Guid.NewGuid().ToString(); - } + } - var userDto = UserFactory.BuildDto(entity); + var userDto = UserFactory.BuildDto(entity); + + //Check if we have a known config, we only want to store config for hashing + //TODO: This logic will need to be updated when we do http://issues.umbraco.org/issue/U4-10089 + var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider(); + if (membershipProvider.PasswordFormat == MembershipPasswordFormat.Hashed) + { + var json = JsonConvert.SerializeObject(new { hashAlgorithm = Membership.HashAlgorithmType }); + userDto.PasswordConfig = json; + } var id = Convert.ToInt32(Database.Insert(userDto)); entity.Id = id; @@ -319,6 +331,15 @@ ORDER BY colName"; userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); changedCols.Add("securityStampToken"); } + + //Check if we have a known config, we only want to store config for hashing + //TODO: This logic will need to be updated when we do http://issues.umbraco.org/issue/U4-10089 + var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider(); + if (membershipProvider.PasswordFormat == MembershipPasswordFormat.Hashed) + { + var json = JsonConvert.SerializeObject(new { hashAlgorithm = Membership.HashAlgorithmType }); + userDto.PasswordConfig = json; + } } //only update the changed cols From e9e2a86e2e68f52b8ee3f2e12fd6949638cd3920 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 20 Jul 2017 15:47:26 +1000 Subject: [PATCH 417/714] U4-5546 Remove user 'channels', weblog api, and all code / db tables relevant to those --- src/Umbraco.Web.UI/web.Template.Debug.config | 6 + src/Umbraco.Web.UI/web.Template.config | 6 - src/Umbraco.Web/Umbraco.Web.csproj | 18 - src/Umbraco.Web/packages.config | 1 - .../umbraco/channels/Blogger.cs | 161 ----- .../umbraco/channels/DocumentDateComparer.cs | 48 -- .../umbraco/channels/MetaWeblog.cs | 139 ----- .../umbraco/channels/UmbracoMetaWeblogAPI.cs | 568 ------------------ .../umbraco/channels/api.cs | 223 ------- .../umbraco/channels/config.cs | 239 -------- .../umbraco/channels/remixWeblogApi.cs | 90 --- .../umbraco/channels/rsd.aspx.cs | 22 - .../umbraco/channels/wlwmanifest.aspx.cs | 39 -- .../umbraco/channels/wordApi.cs | 32 - 14 files changed, 6 insertions(+), 1586 deletions(-) delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/Blogger.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/DocumentDateComparer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/MetaWeblog.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/config.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/remixWeblogApi.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/rsd.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/wlwmanifest.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/channels/wordApi.cs diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 22cdd682fe..4b15922671 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -110,6 +110,8 @@ + + @@ -324,6 +326,10 @@ + + + + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 3a484944e6..837be91b66 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -117,8 +117,6 @@ - - @@ -291,16 +289,12 @@ - - - - diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index afa4790262..4327a96baa 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -109,10 +109,6 @@ ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll True - - ..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll - True - ..\packages\dotless.1.5.2\lib\dotless.Core.dll @@ -1146,12 +1142,6 @@ - - ASPXCodeBehind - - - ASPXCodeBehind - ASPXCodeBehind @@ -1358,14 +1348,6 @@ publish.aspx - - - - - - - - diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index daef6ee7fd..fb32b8d6a8 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -29,5 +29,4 @@ - \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/Blogger.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/Blogger.cs deleted file mode 100644 index 280be0f5b9..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/Blogger.cs +++ /dev/null @@ -1,161 +0,0 @@ - -using CookComputing.XmlRpc; - -namespace CookComputing.Blogger -{ - /// - /// Struct representing a blog category - /// - public struct Category - { - public string categoryid; - public string title; - public string description; - public string htmlUrl; - public string rssUrl; - } - - /// - /// Struct representing a blog post - /// - public struct Post - { - public System.DateTime dateCreated; - [XmlRpcMember( - Description = "Depending on server may be either string or integer. " - + "Use Convert.ToInt32(userid) to treat as integer or " - + "Convert.ToString(userid) to treat as string")] - - //Livejournal sometimes drops these - [XmlRpcMissingMapping(MappingAction.Ignore)] - public object userid; - [XmlRpcMissingMapping(MappingAction.Ignore)] - public string postid; - public string content; - } - - /// - /// Struct containing user information - /// - public struct UserInfo - { - public string url; - public string email; - public string nickname; - public string lastname; - public string firstname; - } - - /// - /// Struct containing Blog information - /// - public struct BlogInfo - { - public string blogid; - public string url; - public string blogName; - } - - public interface IBlogger - { - [XmlRpcMethod("blogger.deletePost", - Description = "Deletes a post.")] - [return: XmlRpcReturnValue(Description = "Always returns true.")] - bool deletePost( - string appKey, - string postid, - string username, - string password, - [XmlRpcParameter( - Description = "Where applicable, this specifies whether the blog " - + "should be republished after the post has been deleted.")] - bool publish); - - [XmlRpcMethod("blogger.editPost", - Description = "Edits a given post. Optionally, will publish the " - + "blog after making the edit.")] - [return: XmlRpcReturnValue(Description = "Always returns true.")] - object editPost( - string appKey, - string postid, - string username, - string password, - string content, - bool publish); - - [XmlRpcMethod("blogger.getCategories", - Description = "Returns a list of the categories that you can use " - + "to log against a post.")] - Category[] getCategories( - string blogid, - string username, - string password); - - [XmlRpcMethod("blogger.getPost", - Description = "Returns a single post.")] - Post getPost( - string appKey, - string postid, - string username, - string password); - - [XmlRpcMethod("blogger.getRecentPosts", - Description = "Returns a list of the most recent posts in the system.")] - Post[] getRecentPosts( - string appKey, - string blogid, - string username, - string password, - int numberOfPosts); - - [XmlRpcMethod("blogger.getTemplate", - Description = "Returns the main or archive index template of " - + "a given blog.")] - string getTemplate( - string appKey, - string blogid, - string username, - string password, - string templateType); - - [XmlRpcMethod("blogger.getUserInfo", - Description = "Authenticates a user and returns basic user info " - + "(name, email, userid, etc.).")] - UserInfo getUserInfo( - string appKey, - string username, - string password); - - [XmlRpcMethod("blogger.getUsersBlogs", - Description = "Returns information on all the blogs a given user " - + "is a member.")] - BlogInfo[] getUsersBlogs( - string appKey, - string username, - string password); - - [XmlRpcMethod("blogger.newPost", - Description = "Makes a new post to a designated blog. Optionally, " - + "will publish the blog after making the post.")] - [return: XmlRpcReturnValue(Description = "Id of new post")] - string newPost( - string appKey, - string blogid, - string username, - string password, - string content, - bool publish); - - [XmlRpcMethod("blogger.setTemplate", - Description = "Edits the main or archive index template of a given blog.")] - bool setTemplate( - string appKey, - string blogid, - string username, - string password, - string template, - string templateType); - } -} - - diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/DocumentDateComparer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/DocumentDateComparer.cs deleted file mode 100644 index f5951b9ffb..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/DocumentDateComparer.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections; -using System.Text; -using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.web; -namespace umbraco.presentation.channels -{ - class DocumentDateComparer : IComparer - { - - int IComparer.Compare(Object x, Object y) - { - - if (((Document)x).CreateDateTime > ((Document)y).CreateDateTime) - { - return -1; - } - else - { - return 0; - } - - } - - } - - - class DocumentSortOrderComparer : IComparer - { - - int IComparer.Compare(Object x, Object y) - { - - if (((CMSNode)x).sortOrder > ((CMSNode)y).sortOrder) - { - return -1; - } - else - { - return 0; - } - - } - - } - - -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/MetaWeblog.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/MetaWeblog.cs deleted file mode 100644 index 74b7d5d724..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/MetaWeblog.cs +++ /dev/null @@ -1,139 +0,0 @@ - -using System; -using CookComputing.XmlRpc; - -namespace CookComputing.MetaWeblog -{ - [XmlRpcMissingMapping(MappingAction.Ignore)] - public struct Enclosure - { - public int length; - public string type; - public string url; - } - - [XmlRpcMissingMapping(MappingAction.Ignore)] - public struct Source - { - public string name; - public string url; - } - - [XmlRpcMissingMapping(MappingAction.Ignore)] - public struct Post - { - [XmlRpcMissingMapping(MappingAction.Error)] - [XmlRpcMember(Description = "Required when posting.")] - public DateTime dateCreated; - [XmlRpcMissingMapping(MappingAction.Error)] - [XmlRpcMember(Description = "Required when posting.")] - public string description; - [XmlRpcMissingMapping(MappingAction.Error)] - [XmlRpcMember(Description = "Required when posting.")] - public string title; - - public string[] categories; - public Enclosure enclosure; - public string link; - public string permalink; - [XmlRpcMember( - Description = "Not required when posting. Depending on server may " - + "be either string or integer. " - + "Use Convert.ToInt32(postid) to treat as integer or " - + "Convert.ToString(postid) to treat as string")] - public object postid; - public Source source; - public string userid; - - public object mt_allow_comments; - public object mt_allow_pings; - public object mt_convert_breaks; - public string mt_text_more; - public string mt_excerpt; - } - - public struct CategoryInfo - { - public string description; - public string htmlUrl; - public string rssUrl; - public string title; - public string categoryid; - } - - public struct Category - { - public string categoryId; - public string categoryName; - } - - public struct FileData - { - public byte[] bits; - public string name; - public string type; - } - - public struct UrlData - { - public string url; - } - - public struct MediaObjectInfo - { - public string url; - } - - public interface IMetaWeblog - { - [XmlRpcMethod("metaWeblog.editPost", - Description = "Updates and existing post to a designated blog " - + "using the metaWeblog API. Returns true if completed.")] - object editPost( - string postid, - string username, - string password, - Post post, - bool publish); - - [XmlRpcMethod("metaWeblog.getCategories", - Description = "Retrieves a list of valid categories for a post " - + "using the metaWeblog API. Returns the metaWeblog categories " - + "struct collection.")] - CategoryInfo[] getCategories( - string blogid, - string username, - string password); - - [XmlRpcMethod("metaWeblog.getPost", - Description = "Retrieves an existing post using the metaWeblog " - + "API. Returns the metaWeblog struct.")] - Post getPost( - string postid, - string username, - string password); - - [XmlRpcMethod("metaWeblog.getRecentPosts", - Description = "Retrieves a list of the most recent existing post " - + "using the metaWeblog API. Returns the metaWeblog struct collection.")] - Post[] getRecentPosts( - string blogid, - string username, - string password, - int numberOfPosts); - - [XmlRpcMethod("metaWeblog.newPost", - Description = "Makes a new post to a designated blog using the " - + "metaWeblog API. Returns postid as a string.")] - string newPost( - string blogid, - string username, - string password, - Post post, - bool publish); - - - } -} - - diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs deleted file mode 100644 index b4fa6ed573..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs +++ /dev/null @@ -1,568 +0,0 @@ -using System; -using System.Collections; -using System.Drawing; -using System.IO; -using System.Web; -using CookComputing.Blogger; -using CookComputing.MetaWeblog; -using CookComputing.XmlRpc; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.datatype; -using umbraco.cms.businesslogic.media; -using umbraco.cms.businesslogic.property; -using umbraco.cms.businesslogic.propertytype; -using umbraco.cms.businesslogic.web; -using Umbraco.Core.Logging; -using Umbraco.Core.Security; -using umbraco.presentation.channels.businesslogic; -using Post = CookComputing.MetaWeblog.Post; - -using System.Collections.Generic; -using System.Web.Security; -using Umbraco.Core.IO; -using Umbraco.Core; - -namespace umbraco.presentation.channels -{ - public abstract class UmbracoMetaWeblogAPI : XmlRpcService, IMetaWeblog - { - internal readonly MediaFileSystem _fs; - - protected UmbracoMetaWeblogAPI() - { - _fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - } - - [XmlRpcMethod("blogger.deletePost", - Description = "Deletes a post.")] - [return: XmlRpcReturnValue(Description = "Always returns true.")] - public bool deletePost( - string appKey, - string postid, - string username, - string password, - [XmlRpcParameter( - Description = "Where applicable, this specifies whether the blog " - + "should be republished after the post has been deleted.")] bool publish) - { - if (ValidateUser(username, password)) - { - Channel userChannel = new Channel(username); - new Document(int.Parse(postid)) - .delete(); - return true; - } - return false; - } - - public object editPost( - string postid, - string username, - string password, - Post post, - bool publish) - { - if (ValidateUser(username, password)) - { - Channel userChannel = new Channel(username); - Document doc = new Document(Convert.ToInt32(postid)); - - - doc.Text = HttpContext.Current.Server.HtmlDecode(post.title); - - // Excerpt - if (userChannel.FieldExcerptAlias != null && userChannel.FieldExcerptAlias != "") - doc.getProperty(userChannel.FieldExcerptAlias).Value = RemoveLeftUrl(post.mt_excerpt); - - - if (UmbracoConfig.For.UmbracoSettings().Content.TidyEditorContent) - doc.getProperty(userChannel.FieldDescriptionAlias).Value = library.Tidy(RemoveLeftUrl(post.description), false); - else - doc.getProperty(userChannel.FieldDescriptionAlias).Value = RemoveLeftUrl(post.description); - - UpdateCategories(doc, post, userChannel); - - - if (publish) - { - doc.SaveAndPublish(new User(username)); - } - return true; - } - else - { - return false; - } - } - - private static void UpdateCategories(Document doc, Post post, Channel userChannel) - { - if (userChannel.FieldCategoriesAlias != null && userChannel.FieldCategoriesAlias != "") - { - ContentType blogPostType = ContentType.GetByAlias(userChannel.DocumentTypeAlias); - PropertyType categoryType = blogPostType.getPropertyType(userChannel.FieldCategoriesAlias); - - String[] categories = post.categories; - string categoryValue = ""; - interfaces.IUseTags tags = UseTags(categoryType); - if (tags != null) - { - tags.RemoveTagsFromNode(doc.Id); - for (int i = 0; i < categories.Length; i++) - { - tags.AddTagToNode(doc.Id, categories[i]); - } - //If the IUseTags provider manually set the property value to something on the IData interface then we should persist this - //code commented as for some reason, even though the IUseTags control is setting IData.Value it is null here - //could be a cache issue, or maybe it's a different instance of the IData or something, rather odd - //doc.getProperty(userChannel.FieldCategoriesAlias).Value = categoryType.DataTypeDefinition.DataType.Data.Value; - - //Instead, set the document property to CSV of the tags - this WILL break custom editors for tags which don't adhere to the - //pseudo standard that the .Value of the property contains CSV tags. - doc.getProperty(userChannel.FieldCategoriesAlias).Value = string.Join(",", categories); - } - else - { - for (int i = 0; i < categories.Length; i++) - { - PreValue pv = new PreValue(categoryType.DataTypeDefinition.Id, categories[i]); - categoryValue += pv.Id + ","; - } - if (categoryValue.Length > 0) - categoryValue = categoryValue.Substring(0, categoryValue.Length - 1); - - doc.getProperty(userChannel.FieldCategoriesAlias).Value = categoryValue; - } - } - } - - public CategoryInfo[] getCategories( - string blogid, - string username, - string password) - { - if (ValidateUser(username, password)) - { - Channel userChannel = new Channel(username); - if (userChannel.FieldCategoriesAlias != null && userChannel.FieldCategoriesAlias != "") - { - // Find the propertytype via the document type - ContentType blogPostType = ContentType.GetByAlias(userChannel.DocumentTypeAlias); - PropertyType categoryType = blogPostType.getPropertyType(userChannel.FieldCategoriesAlias); - - // check if the datatype uses tags or prevalues - CategoryInfo[] returnedCategories = null; - interfaces.IUseTags tags = UseTags(categoryType); - if (tags != null) - { - List alltags = tags.GetAllTags(); - if (alltags != null) - { - returnedCategories = new CategoryInfo[alltags.Count]; - int counter = 0; - foreach (interfaces.ITag t in alltags) - { - CategoryInfo ci = new CategoryInfo(); - ci.title = t.TagCaption; - ci.categoryid = t.Id.ToString(); - ci.description = ""; - ci.rssUrl = ""; - ci.htmlUrl = ""; - returnedCategories[counter] = ci; - counter++; - } - } - else - { - returnedCategories = new CategoryInfo[0]; - } - } - else - { - SortedList categories = PreValues.GetPreValues(categoryType.DataTypeDefinition.Id); - returnedCategories = new CategoryInfo[categories.Count]; - IDictionaryEnumerator ide = categories.GetEnumerator(); - int counter = 0; - while (ide.MoveNext()) - { - PreValue category = (PreValue)ide.Value; - CategoryInfo ci = new CategoryInfo(); - ci.title = category.Value; - ci.categoryid = category.Id.ToString(); - ci.description = ""; - ci.rssUrl = ""; - ci.htmlUrl = ""; - returnedCategories[counter] = ci; - counter++; - } - } - - return returnedCategories; - } - } - - throw new ArgumentException("Categories doesn't work for this channel, they might not have been activated. Contact your umbraco administrator."); - } - - public static interfaces.IUseTags UseTags(PropertyType categoryType) - { - if (typeof(interfaces.IUseTags).IsAssignableFrom(categoryType.DataTypeDefinition.DataType.DataEditor.GetType())) - { - interfaces.IUseTags tags = (interfaces.IUseTags)categoryType.DataTypeDefinition.DataType.DataEditor as interfaces.IUseTags; - return tags; - } - return null; - } - - public Post getPost( - string postid, - string username, - string password) - { - if (ValidateUser(username, password)) - { - Channel userChannel = new Channel(username); - Document d = new Document(int.Parse(postid)); - Post p = new Post(); - p.title = d.Text; - p.description = d.getProperty(userChannel.FieldDescriptionAlias).Value.ToString(); - - // Excerpt - if (userChannel.FieldExcerptAlias != null && userChannel.FieldExcerptAlias != "") - p.mt_excerpt = d.getProperty(userChannel.FieldExcerptAlias).Value.ToString(); - - // Categories - if (userChannel.FieldCategoriesAlias != null && userChannel.FieldCategoriesAlias != "" && - d.getProperty(userChannel.FieldCategoriesAlias) != null && - d.getProperty(userChannel.FieldCategoriesAlias).Value != null && - d.getProperty(userChannel.FieldCategoriesAlias).Value.ToString() != "") - { - String categories = d.getProperty(userChannel.FieldCategoriesAlias).Value.ToString(); - char[] splitter = { ',' }; - String[] categoryIds = categories.Split(splitter); - p.categories = categoryIds; - } - - p.postid = postid; - p.permalink = library.NiceUrl(d.Id); - p.dateCreated = d.CreateDateTime; - p.link = p.permalink; - return p; - } - else - throw new ArgumentException(string.Format("Error retriving post with id: '{0}'", postid)); - } - - public Post[] getRecentPosts( - string blogid, - string username, - string password, - int numberOfPosts) - { - if (ValidateUser(username, password)) - { - ArrayList blogPosts = new ArrayList(); - ArrayList blogPostsObjects = new ArrayList(); - - User u = new User(username); - Channel userChannel = new Channel(u.Id); - - - Document rootDoc; - if (userChannel.StartNode > 0) - rootDoc = new Document(userChannel.StartNode); - else - { - if (u.StartNodeId == -1) - { - rootDoc = Document.GetRootDocuments()[0]; - } - else - { - rootDoc = new Document(u.StartNodeId); - } - } - - //store children array here because iterating over an Array object is very inneficient. - var c = rootDoc.Children; - foreach (Document d in c) - { - int count = 0; - blogPosts.AddRange( - findBlogPosts(userChannel, d, u.Name, ref count, numberOfPosts, userChannel.FullTree)); - } - - blogPosts.Sort(new DocumentSortOrderComparer()); - - foreach (Object o in blogPosts) - { - Document d = (Document)o; - Post p = new Post(); - p.dateCreated = d.CreateDateTime; - p.userid = username; - p.title = d.Text; - p.permalink = library.NiceUrl(d.Id); - p.description = d.getProperty(userChannel.FieldDescriptionAlias).Value.ToString(); - p.link = library.NiceUrl(d.Id); - p.postid = d.Id.ToString(); - - if (userChannel.FieldCategoriesAlias != null && userChannel.FieldCategoriesAlias != "" && - d.getProperty(userChannel.FieldCategoriesAlias) != null && - d.getProperty(userChannel.FieldCategoriesAlias).Value != null && - d.getProperty(userChannel.FieldCategoriesAlias).Value.ToString() != "") - { - String categories = d.getProperty(userChannel.FieldCategoriesAlias).Value.ToString(); - char[] splitter = { ',' }; - String[] categoryIds = categories.Split(splitter); - p.categories = categoryIds; - } - - // Excerpt - if (userChannel.FieldExcerptAlias != null && userChannel.FieldExcerptAlias != "") - p.mt_excerpt = d.getProperty(userChannel.FieldExcerptAlias).Value.ToString(); - - - blogPostsObjects.Add(p); - } - - - return (Post[])blogPostsObjects.ToArray(typeof(Post)); - } - else - { - return null; - } - } - - protected ArrayList findBlogPosts(Channel userChannel, Document d, String userName, ref int count, int max, - bool fullTree) - { - ArrayList list = new ArrayList(); - - ContentType ct = d.ContentType; - - if (ct.Alias.Equals(userChannel.DocumentTypeAlias) & - (count < max)) - { - list.Add(d); - count = count + 1; - } - - if (d.Children != null && d.Children.Length > 0 && fullTree) - { - //store children array here because iterating over an Array object is very inneficient. - var c = d.Children; - foreach (Document child in c) - { - if (count < max) - { - list.AddRange(findBlogPosts(userChannel, child, userName, ref count, max, true)); - } - } - } - return list; - } - - public string newPost( - string blogid, - string username, - string password, - Post post, - bool publish) - { - if (ValidateUser(username, password)) - { - Channel userChannel = new Channel(username); - User u = new User(username); - Document doc = - Document.MakeNew(HttpContext.Current.Server.HtmlDecode(post.title), - DocumentType.GetByAlias(userChannel.DocumentTypeAlias), u, - userChannel.StartNode); - - - // Excerpt - if (userChannel.FieldExcerptAlias != null && userChannel.FieldExcerptAlias != "") - doc.getProperty(userChannel.FieldExcerptAlias).Value = RemoveLeftUrl(post.mt_excerpt); - - - // Description - if (UmbracoConfig.For.UmbracoSettings().Content.TidyEditorContent) - doc.getProperty(userChannel.FieldDescriptionAlias).Value = library.Tidy(RemoveLeftUrl(post.description), false); - else - doc.getProperty(userChannel.FieldDescriptionAlias).Value = RemoveLeftUrl(post.description); - - // Categories - UpdateCategories(doc, post, userChannel); - - // check release date - if (post.dateCreated.Year > 0001) - { - publish = false; - doc.ReleaseDate = post.dateCreated; - } - - if (publish) - { - doc.SaveAndPublish(new User(username)); - } - return doc.Id.ToString(); - } - else - throw new ArgumentException("Error creating post"); - } - - protected MediaObjectInfo newMediaObjectLogicForWord( - string blogid, - string username, - string password, - FileData file) - { - UrlData ud = newMediaObjectLogic(blogid, username, password, file); - MediaObjectInfo moi = new MediaObjectInfo(); - moi.url = ud.url; - return moi; - } - protected UrlData newMediaObjectLogic( - string blogid, - string username, - string password, - FileData file) - { - if (ValidateUser(username, password)) - { - User u = new User(username); - Channel userChannel = new Channel(username); - UrlData fileUrl = new UrlData(); - if (userChannel.ImageSupport) - { - Media rootNode; - if (userChannel.MediaFolder > 0) - rootNode = new Media(userChannel.MediaFolder); - else - rootNode = new Media(u.StartMediaId); - - // Create new media - Media m = Media.MakeNew(file.name, MediaType.GetByAlias(userChannel.MediaTypeAlias), u, rootNode.Id); - - Property fileObject = m.getProperty(userChannel.MediaTypeFileProperty); - - var filename = file.name.Replace("/", "_"); - var relativeFilePath = UmbracoMediaFactory.GetRelativePath(fileObject.Id, filename); - - fileObject.Value = _fs.GetUrl(relativeFilePath); - fileUrl.url = fileObject.Value.ToString(); - - if (!fileUrl.url.StartsWith("http")) - { - var protocol = GlobalSettings.UseSSL ? "https" : "http"; - fileUrl.url = protocol + "://" + HttpContext.Current.Request.ServerVariables["SERVER_NAME"] + fileUrl.url; - } - - _fs.AddFile(relativeFilePath, new MemoryStream(file.bits)); - - // Try updating standard file values - try - { - string orgExt = ""; - // Size - if (m.getProperty(Constants.Conventions.Media.Bytes) != null) - m.getProperty(Constants.Conventions.Media.Bytes).Value = file.bits.Length; - // Extension - if (m.getProperty(Constants.Conventions.Media.Extension) != null) - { - orgExt = - ((string) - file.name.Substring(file.name.LastIndexOf(".") + 1, - file.name.Length - file.name.LastIndexOf(".") - 1)); - m.getProperty(Constants.Conventions.Media.Extension).Value = orgExt.ToLower(); - } - // Width and Height - // Check if image and then get sizes, make thumb and update database - if (m.getProperty(Constants.Conventions.Media.Width) != null && m.getProperty(Constants.Conventions.Media.Height) != null && - ",jpeg,jpg,gif,bmp,png,tiff,tif,".IndexOf("," + orgExt.ToLower() + ",") > 0) - { - int fileWidth; - int fileHeight; - - using (var stream = _fs.OpenFile(relativeFilePath)) - { - Image image = Image.FromStream(stream); - fileWidth = image.Width; - fileHeight = image.Height; - stream.Close(); - try - { - m.getProperty(Constants.Conventions.Media.Width).Value = fileWidth.ToString(); - m.getProperty(Constants.Conventions.Media.Height).Value = fileHeight.ToString(); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred reading the media stream", ex); - } - } - - - } - } - catch (Exception ex) - { - LogHelper.Error("An error occurred in newMediaObjectLogic", ex); - } - - return fileUrl; - } - else - throw new ArgumentException( - "Image Support is turned off in this channel. Modify channel settings in umbraco to enable image support."); - } - return new UrlData(); - } - - private static bool ValidateUser(string username, string password) - { - var provider = MembershipProviderExtensions.GetUsersMembershipProvider(); - - return provider.ValidateUser(username, password); - } - - [XmlRpcMethod("blogger.getUsersBlogs", - Description = "Returns information on all the blogs a given user " - + "is a member.")] - public BlogInfo[] getUsersBlogs( - string appKey, - string username, - string password) - { - if (ValidateUser(username, password)) - { - BlogInfo[] blogs = new BlogInfo[1]; - User u = new User(username); - Channel userChannel = new Channel(u.Id); - Document rootDoc; - if (userChannel.StartNode > 0) - rootDoc = new Document(userChannel.StartNode); - else - rootDoc = new Document(u.StartNodeId); - - BlogInfo bInfo = new BlogInfo(); - bInfo.blogName = userChannel.Name; - bInfo.blogid = rootDoc.Id.ToString(); - bInfo.url = library.NiceUrlWithDomain(rootDoc.Id, true); - blogs[0] = bInfo; - - return blogs; - } - - throw new ArgumentException(string.Format("No data found for user with username: '{0}'", username)); - } - - private static string RemoveLeftUrl(string text) - { - return - text.Replace(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority), ""); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs deleted file mode 100644 index ace78f6803..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections; -using CookComputing.MetaWeblog; -using CookComputing.XmlRpc; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.datatype; -using umbraco.cms.businesslogic.propertytype; -using umbraco.cms.businesslogic.web; -using umbraco.presentation.channels.businesslogic; - -namespace umbraco.presentation.channels -{ - /// - /// the umbraco channels API is xml-rpc webservice based on the metaweblog and blogger APIs - /// for editing umbraco data froom external clients - /// - [XmlRpcService( - Name = "umbraco metablog api", - Description = "For editing umbraco data from external clients", - AutoDocumentation = true)] - public class api : UmbracoMetaWeblogAPI, IRemixWeblogApi - { - /// - /// Initializes a new instance of the class. - /// - public api() - { - } - - - /// - /// Makes a new file to a designated blog using the metaWeblog API - /// - /// The blogid. - /// The username. - /// The password. - /// The file. - /// Returns url as a string of a struct. - [XmlRpcMethod("metaWeblog.newMediaObject", - Description = "Makes a new file to a designated blog using the " - + "metaWeblog API. Returns url as a string of a struct.")] - public UrlData newMediaObject( - string blogid, - string username, - string password, - FileData file) - { - return newMediaObjectLogic(blogid, username, password, file); - } - - #region IRemixWeblogApi Members - - /// - /// Gets a summary of all the pages from the blog with the spefied blogId. - /// - /// The blogid. - /// The username. - /// The password. - /// - public wpPageSummary[] getPageList(string blogid, string username, string password) - { - if (User.validateCredentials(username, password, false)) - { - ArrayList blogPosts = new ArrayList(); - ArrayList blogPostsObjects = new ArrayList(); - - User u = new User(username); - Channel userChannel = new Channel(u.Id); - - - Document rootDoc; - if (userChannel.StartNode > 0) - rootDoc = new Document(userChannel.StartNode); - else - rootDoc = new Document(u.StartNodeId); - - //store children array here because iterating over an Array object is very inneficient. - var c = rootDoc.Children; - foreach (Document d in c) - { - int count = 0; - blogPosts.AddRange( - findBlogPosts(userChannel, d, u.Name, ref count, 999, userChannel.FullTree)); - } - - blogPosts.Sort(new DocumentSortOrderComparer()); - - foreach (Object o in blogPosts) - { - Document d = (Document)o; - wpPageSummary p = new wpPageSummary(); - p.dateCreated = d.CreateDateTime; - p.page_title = d.Text; - p.page_id = d.Id; - p.page_parent_id = d.ParentId; - - blogPostsObjects.Add(p); - } - - - return (wpPageSummary[])blogPostsObjects.ToArray(typeof(wpPageSummary)); - } - else - { - return null; - } - } - - /// - /// Gets a specified number of pages from the blog with the spefied blogId - /// - /// The blogid. - /// The username. - /// The password. - /// The number of pages. - /// - public wpPage[] getPages(string blogid, string username, string password, int numberOfItems) - { - if (User.validateCredentials(username, password, false)) - { - ArrayList blogPosts = new ArrayList(); - ArrayList blogPostsObjects = new ArrayList(); - - User u = new User(username); - Channel userChannel = new Channel(u.Id); - - - Document rootDoc; - if (userChannel.StartNode > 0) - rootDoc = new Document(userChannel.StartNode); - else - rootDoc = new Document(u.StartNodeId); - - //store children array here because iterating over an Array object is very inneficient. - var c = rootDoc.Children; - foreach (Document d in c) - { - int count = 0; - blogPosts.AddRange( - findBlogPosts(userChannel, d, u.Name, ref count, numberOfItems, userChannel.FullTree)); - } - - blogPosts.Sort(new DocumentSortOrderComparer()); - - foreach (Object o in blogPosts) - { - Document d = (Document)o; - wpPage p = new wpPage(); - p.dateCreated = d.CreateDateTime; - p.title = d.Text; - p.page_id = d.Id; - p.wp_page_parent_id = d.ParentId; - p.wp_page_parent_title = d.Parent.Text; - p.permalink = library.NiceUrl(d.Id); - p.description = d.getProperty(userChannel.FieldDescriptionAlias).Value.ToString(); - p.link = library.NiceUrl(d.Id); - - if (userChannel.FieldCategoriesAlias != null && userChannel.FieldCategoriesAlias != "" && - d.getProperty(userChannel.FieldCategoriesAlias) != null && - ((string)d.getProperty(userChannel.FieldCategoriesAlias).Value) != "") - { - String categories = d.getProperty(userChannel.FieldCategoriesAlias).Value.ToString(); - char[] splitter = { ',' }; - String[] categoryIds = categories.Split(splitter); - p.categories = categoryIds; - } - - - blogPostsObjects.Add(p); - } - - - return (wpPage[])blogPostsObjects.ToArray(typeof(wpPage)); - } - else - { - return null; - } - } - - /// - /// Creates a new blog category / tag. - /// - /// The blogid. - /// The username. - /// The password. - /// The category. - /// - public string newCategory( - string blogid, - string username, - string password, - wpCategory category) - { - if (User.validateCredentials(username, password, false)) - { - Channel userChannel = new Channel(username); - if (userChannel.FieldCategoriesAlias != null && userChannel.FieldCategoriesAlias != "") - { - // Find the propertytype via the document type - ContentType blogPostType = ContentType.GetByAlias(userChannel.DocumentTypeAlias); - PropertyType categoryType = blogPostType.getPropertyType(userChannel.FieldCategoriesAlias); - interfaces.IUseTags tags = UseTags(categoryType); - if (tags != null) - { - tags.AddTag(category.name); - } - else - { - PreValue pv = new PreValue(); - pv.DataTypeId = categoryType.DataTypeDefinition.Id; - pv.Value = category.name; - pv.Save(); - } - } - } - return ""; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/config.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/config.cs deleted file mode 100644 index 3cb25cb765..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/config.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.IO; -using System.Web; -using System.Xml; -using umbraco.BusinessLogic; -using Umbraco.Core.IO; - -namespace umbraco.presentation.channels.businesslogic -{ - public class config - { - private static XmlDocument _metablogConfigFile; - - public static XmlDocument MetaBlogConfigFile - { - get - { - if (_metablogConfigFile == null) - { - _metablogConfigFile = new XmlDocument(); - _metablogConfigFile.Load(IOHelper.MapPath(SystemFiles.MetablogConfig)); - } - - return _metablogConfigFile; - } - } - } - - public class Channel - { - public Channel(string username) - { - User u = new User(username); - initialize(u.Id); - } - - public Channel(int UserId) - { - initialize(UserId); - } - - private void initialize(int UserId) - { - XmlDocument configFile = config.MetaBlogConfigFile; - XmlNode channelXml = configFile.SelectSingleNode(string.Format("//channel [user = '{0}']", UserId)); - if (channelXml != null) - { - Id = UserId; - User = new User(UserId); - Name = channelXml.SelectSingleNode("./name").FirstChild.Value; - StartNode = int.Parse(channelXml.SelectSingleNode("./startNode").FirstChild.Value); - FullTree = bool.Parse(channelXml.SelectSingleNode("./fullTree").FirstChild.Value); - DocumentTypeAlias = channelXml.SelectSingleNode("./documentTypeAlias").FirstChild.Value; - if (channelXml.SelectSingleNode("./fields/categories").FirstChild != null) - FieldCategoriesAlias = channelXml.SelectSingleNode("./fields/categories").FirstChild.Value; - if (channelXml.SelectSingleNode("./fields/description").FirstChild != null) - FieldDescriptionAlias = channelXml.SelectSingleNode("./fields/description").FirstChild.Value; - if (channelXml.SelectSingleNode("./fields/excerpt") != null && channelXml.SelectSingleNode("./fields/excerpt").FirstChild != null) - FieldExcerptAlias = channelXml.SelectSingleNode("./fields/excerpt").FirstChild.Value; - - XmlNode mediaSupport = channelXml.SelectSingleNode("./mediaObjectSupport"); - ImageSupport = bool.Parse(mediaSupport.Attributes.GetNamedItem("enabled").Value); - MediaFolder = int.Parse(mediaSupport.Attributes.GetNamedItem("folderId").Value); - MediaTypeAlias = mediaSupport.Attributes.GetNamedItem("mediaTypeAlias").Value; - MediaTypeFileProperty = mediaSupport.Attributes.GetNamedItem("mediaTypeFileProperty").Value; - } - else - throw new ArgumentException(string.Format("No channel found for user with id: '{0}'", UserId)); - } - - public Channel() - { - } - - public void Save() - { - // update node - XmlDocument configFile = config.MetaBlogConfigFile; - XmlNode channelXml = null; - if (User != null && User.Id > -1) - channelXml = configFile.SelectSingleNode(string.Format("//channel [user = '{0}']", this.User.Id)); - if (channelXml != null) - configFile.DocumentElement.RemoveChild(channelXml); - - // add new node - XmlElement newChannelxml = configFile.CreateElement("channel"); - newChannelxml.AppendChild( - xmlHelper.addTextNode(configFile, "name", Name)); - newChannelxml.AppendChild( - xmlHelper.addTextNode(configFile, "user", User.Id.ToString())); - newChannelxml.AppendChild( - xmlHelper.addTextNode(configFile, "startNode", StartNode.ToString())); - newChannelxml.AppendChild( - xmlHelper.addTextNode(configFile, "fullTree", FullTree.ToString())); - newChannelxml.AppendChild( - xmlHelper.addTextNode(configFile, "documentTypeAlias", DocumentTypeAlias)); - - // fields - XmlElement fieldsxml = configFile.CreateElement("fields"); - fieldsxml.AppendChild( - xmlHelper.addTextNode(configFile, "categories", FieldCategoriesAlias)); - fieldsxml.AppendChild( - xmlHelper.addTextNode(configFile, "description", FieldDescriptionAlias)); - fieldsxml.AppendChild( - xmlHelper.addTextNode(configFile, "excerpt", FieldExcerptAlias)); - newChannelxml.AppendChild(fieldsxml); - - - // media - XmlElement media = configFile.CreateElement("mediaObjectSupport"); - media.Attributes.Append(xmlHelper.addAttribute(configFile, "enabled", ImageSupport.ToString())); - media.Attributes.Append(xmlHelper.addAttribute(configFile, "folderId", MediaFolder.ToString())); - media.Attributes.Append(xmlHelper.addAttribute(configFile, "mediaTypeAlias", MediaTypeAlias)); - media.Attributes.Append(xmlHelper.addAttribute(configFile, "mediaTypeFileProperty", MediaTypeFileProperty)); - newChannelxml.AppendChild(media); - configFile.DocumentElement.AppendChild(newChannelxml); - - configFile.Save( IOHelper.MapPath( SystemFiles.MetablogConfig )); - - - } - - private string _fieldExcerptAlias; - - public string FieldExcerptAlias - { - get { return _fieldExcerptAlias; } - set { _fieldExcerptAlias = value; } - } - - - private string _mediaTypeFileProperty; - - public string MediaTypeFileProperty - { - get { return _mediaTypeFileProperty; } - set { _mediaTypeFileProperty = value; } - } - - - private string _mediaTypeAlias; - - public string MediaTypeAlias - { - get { return _mediaTypeAlias; } - set { _mediaTypeAlias = value; } - } - - - private int _mediaFolder; - - public int MediaFolder - { - get { return _mediaFolder; } - set { _mediaFolder = value; } - } - - - private bool _imageSupport; - - public bool ImageSupport - { - get { return _imageSupport; } - set { _imageSupport = value; } - } - - - private int _startNode; - - public int StartNode - { - get { return _startNode; } - set { _startNode = value; } - } - - - private int _id; - - public int Id - { - get { return _id; } - set { _id = value; } - } - - - private string _fieldCategoriesAlias; - - public string FieldCategoriesAlias - { - get { return _fieldCategoriesAlias; } - set { _fieldCategoriesAlias = value; } - } - - - private string _fieldDescriptionAlias; - - public string FieldDescriptionAlias - { - get { return _fieldDescriptionAlias; } - set { _fieldDescriptionAlias = value; } - } - - - private string _documentTypeAlias; - - public string DocumentTypeAlias - { - get { return _documentTypeAlias; } - set { _documentTypeAlias = value; } - } - - - private bool _fulltree; - - public bool FullTree - { - get { return _fulltree; } - set { _fulltree = value; } - } - - - private User _user; - - public User User - { - get { return _user; } - set { _user = value; } - } - - - private string _name; - - public string Name - { - get { return _name; } - set { _name = value; } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/remixWeblogApi.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/remixWeblogApi.cs deleted file mode 100644 index 6c9103e669..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/remixWeblogApi.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; -using CookComputing.MetaWeblog; -using CookComputing.XmlRpc; - -namespace umbraco.presentation.channels -{ - - [XmlRpcMissingMapping(MappingAction.Ignore)] - public struct wpPage - { - public DateTime dateCreated; - public int userid; - public int page_id; - public string page_status; - public string description; - public string title; - public string link; - public string permalink; - public string[] categories; - public string excerpt; - public string text_more; - public int mt_allow_comments; - public int mt_allow_pings; - - public string wp_slug; - public string wp_password; - public string wp_author; - public int wp_page_parent_id; - public string wp_page_parent_title; - public int wp_page_order; - public int wp_author_id; - public string wp_author_display_name; - - } - - [XmlRpcMissingMapping(MappingAction.Ignore)] - public struct wpPageSummary - { - public int page_id; - public string page_title; - public int page_parent_id; - public DateTime dateCreated; - - } - [XmlRpcMissingMapping(MappingAction.Ignore)] - public struct wpCategory - { - public string name; - public string slug; - public int parent_id; - string description; - } - - - public interface IRemixWeblogApi - { - [XmlRpcMethod("wp.getPageList", - Description = "Retrieves a list of pages as summary from the current channel")] - wpPageSummary[] getPageList( - string blogid, - string username, - string password); - - [XmlRpcMethod("wp.getPages", - Description = "Retrieves a list of pages from the current channel")] - wpPage[] getPages( - string blogid, - string username, - string password, - int numberOfItems); - - [XmlRpcMethod("wp.newCategory", - Description = "Adds a new category")] - string newCategory( - string blogid, - string username, - string password, - wpCategory category); - - } - -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/rsd.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/rsd.aspx.cs deleted file mode 100644 index 7686a48738..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/rsd.aspx.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Collections; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; - -namespace umbraco.presentation.umbraco.channels -{ - [Obsolete("This class is no longer used and will be removed from the codebase in future versions")] - public partial class rsd : System.Web.UI.Page - { - protected void Page_Load(object sender, EventArgs e) - { - Response.ContentType = "text/xml"; - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/wlwmanifest.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/wlwmanifest.aspx.cs deleted file mode 100644 index 51b7b38335..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/wlwmanifest.aspx.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Collections; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; - -namespace umbraco.presentation.channels -{ - [Obsolete("This class is no longer used and will be removed from the codebase in future versions")] - public partial class wlwmanifest : System.Web.UI.Page - { - protected void Page_Load(object sender, EventArgs e) - { - bool useXhtml = false; - if (bool.TryParse(GlobalSettings.EditXhtmlMode, out useXhtml) && !useXhtml) - { - xhtml.Text = "no"; - } - else - { - xhtml.Text = "yes"; - } - } - - /// - /// xhtml control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal xhtml; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/wordApi.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/wordApi.cs deleted file mode 100644 index cc9fcf1e64..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/wordApi.cs +++ /dev/null @@ -1,32 +0,0 @@ -using CookComputing.MetaWeblog; -using CookComputing.XmlRpc; - -namespace umbraco.presentation.channels -{ - /// - /// Summary description for Test. - /// - [XmlRpcService( - Name = "umbraco metablog test", - Description = "For editing umbraco data from external clients", - AutoDocumentation = true)] - public class wordApi : UmbracoMetaWeblogAPI - { - public wordApi() - { - } - - [XmlRpcMethod("metaWeblog.newMediaObject", - Description = "Makes a new file to a designated blog using the " - + "metaWeblog API. Returns url as a string of a struct.")] - public MediaObjectInfo newMediaObject( - string blogid, - string username, - string password, - FileData file) - { - return newMediaObjectLogicForWord(blogid.ToString(), username, password, file); - } - - } -} \ No newline at end of file From 69fc7c4b7c7a64dba6d027551db338d8fcbea643 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 20 Jul 2017 16:44:28 +1000 Subject: [PATCH 418/714] reverts removing cook computing dll and ensures we still ship with it, removes publish.aspx --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 3 + src/Umbraco.Web.UI/packages.config | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 3 - .../umbraco/dialogs/publish.aspx.cs | 240 ------------------ 4 files changed, 4 insertions(+), 243 deletions(-) delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/publish.aspx.cs diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index cc07498ac2..8bf705f9b7 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -124,6 +124,9 @@ ..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll True + + ..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll + ..\packages\dotless.1.5.2\lib\dotless.Core.dll diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 8b22708406..9f0ad872fd 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -38,4 +38,5 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 4327a96baa..8a35320ffb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1148,9 +1148,6 @@ ASPXCodeBehind - - ASPXCodeBehind - ASPXCodeBehind diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/publish.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/publish.aspx.cs deleted file mode 100644 index 255c61cbe8..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/publish.aspx.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Generic; -using System.Web.UI; -using System.Web.UI.WebControls; -using Umbraco.Core.Logging; -using umbraco.cms.businesslogic.web; -using umbraco.BusinessLogic; -using Umbraco.Core.Logging; -using umbraco.BasePages; - -namespace umbraco.dialogs -{ - /// - /// Summary description for publish. - /// - [Obsolete("This is no longer used whatsoever and will be removed from the codebase")] - public partial class publish : UmbracoEnsuredPage - { - protected Literal total; - - private int _nodeId; - private int _nodesPublished = 0; - private readonly List _documents = new List(); - public static string pageName = ""; - - public publish() - { - CurrentApp = DefaultApps.content.ToString(); - } - - protected void Page_Load(object sender, EventArgs e) - { - _nodeId = int.Parse(helper.Request("id")); - var d = new cms.businesslogic.web.Document(_nodeId); - pageName = d.Text; - - if (d.Level > 1 && d.PathPublished == false) - { - TheForm.Visible = false; - theEnd.Visible = true; - feedbackMsg.type = uicontrols.Feedback.feedbacktype.notice; - feedbackMsg.Text = ui.Text("publish", "contentPublishedFailedByParent", d.Text, getUser()) + "

" + ui.Text("closeThisWindow") + ""; - - return; - } - - // add control prefix to variable for support with masterpages - var prefix = PublishUnpublishedItems.ClientID.Replace(PublishUnpublishedItems.ID, ""); - masterPagePrefix.Text = prefix; - - // by default we only count the published ones - var totalNodesToPublish = cms.businesslogic.web.Document.CountSubs(_nodeId, true); - try - { - Application.Lock(); - // We add both all nodes and only published nodes to the application variables so we can ajax query depending on checkboxes - Application["publishTotalAll" + _nodeId.ToString()] = cms.businesslogic.CMSNode.CountSubs(_nodeId).ToString(); - Application["publishTotal" + _nodeId.ToString()] = totalNodesToPublish.ToString(); - Application["publishDone" + _nodeId.ToString()] = "0"; - } - finally - { - Application.UnLock(); - } - total.Text = totalNodesToPublish.ToString(); - - // Put user code to initialize the page here - if (!IsPostBack) - { - // Add caption to checkbox - PublishAll.Text = ui.Text("publish", "publishAll", d.Text, getUser()); - ok.Text = ui.Text("content", "publish", getUser()); - ok.Attributes.Add("style", "width: 60px"); - ok.Attributes.Add("onClick", "startPublication();"); - - // Add checkbox event, so the publish unpublished childs gets enabled - PublishUnpublishedItems.LabelAttributes.Add("disabled", "true"); - PublishUnpublishedItems.LabelAttributes.Add("id", "publishUnpublishedItemsLabel"); - PublishUnpublishedItems.InputAttributes.Add("disabled", "true"); - PublishAll.InputAttributes.Add("onclick", "togglePublishingModes(this)"); - } - else - { - - if (PublishAll.Checked) - { - _nodesPublished = 0; - - DoPublishSubs(d); - - Application.Lock(); - Application["publishTotal" + _nodeId.ToString()] = 0; - Application.UnLock(); - - feedbackMsg.type = uicontrols.Feedback.feedbacktype.success; - - feedbackMsg.Text = ui.Text("publish", "nodePublishAll", d.Text, getUser()) + "

" + ui.Text("closeThisWindow") + ""; - - ClientTools.ReloadActionNode(true, true); - - Application.Lock(); - - Application["publishTotal" + _nodeId.ToString()] = null; - Application["publishDone" + _nodeId.ToString()] = null; - Application.UnLock(); - } - else - { - if (d.PublishWithResult(getUser())) - { - feedbackMsg.type = uicontrols.Feedback.feedbacktype.success; - feedbackMsg.Text = ui.Text("publish", "nodePublish", d.Text, getUser()) + "

" + ui.Text("closeThisWindow") + ""; - } - else { - feedbackMsg.type = uicontrols.Feedback.feedbacktype.notice; - feedbackMsg.Text = ui.Text("publish", "contentPublishedFailedByEvent", d.Text, getUser()) + "

" + ui.Text("closeThisWindow") + ""; - } - ClientTools.ReloadActionNode(true, false); - } - - TheForm.Visible = false; - theEnd.Visible = true; - } - } - - private void DoPublishSubs(cms.businesslogic.web.Document d) - { - if (d.Published || PublishUnpublishedItems.Checked) - { - if (d.PublishWithResult(UmbracoUser)) - { - - - _nodesPublished++; - Application.Lock(); - Application["publishDone" + _nodeId.ToString()] = _nodesPublished.ToString(); - Application.UnLock(); - foreach (var dc in d.Children) - { - DoPublishSubs(dc); - } - } - else - { - LogHelper.Warn("Publishing failed due to event cancelling the publishing for document " + d.Id); - } - } - } - - protected override void OnPreRender(EventArgs e) - { - base.OnPreRender(e); - - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/publication.asmx")); - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/legacyAjaxCalls.asmx")); - } - - ///

- /// masterPagePrefix control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal masterPagePrefix; - - /// - /// JsInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude1; - - /// - /// TheForm control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel TheForm; - - /// - /// PublishAll control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBox PublishAll; - - /// - /// PublishUnpublishedItems control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBox PublishUnpublishedItems; - - /// - /// ok control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button ok; - - /// - /// ProgBar1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.ProgressBar ProgBar1; - - /// - /// theEnd control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel theEnd; - - /// - /// feedbackMsg control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Feedback feedbackMsg; - } -} From a875ede3da28e99e954cd26dacfbeab202232175 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 20 Jul 2017 18:55:38 +1000 Subject: [PATCH 419/714] Removes a ton of old unused legacy files --- src/Umbraco.Tests/UI/LegacyDialogTests.cs | 3 - .../umbracocontextmenu/editor_plugin_src.js | 69 --- .../plugins/umbracocontextmenu/plugin.min.js | 69 --- .../plugins/umbracocss/img/example.gif | Bin 87 -> 0 bytes .../tinymce/plugins/umbracocss/js/dialog.js | 19 - .../tinymce/plugins/umbracocss/langs/en.js | 3 - .../plugins/umbracocss/langs/en_dlg.js | 3 - .../tinymce/plugins/umbracocss/langs/en_us.js | 3 - .../plugins/umbracocss/langs/en_us_dlg.js | 3 - .../tinymce/plugins/umbracocss/langs/it.js | 3 - .../plugins/umbracocss/langs/it_dlg.js | 3 - .../tinymce/plugins/umbracocss/langs/ja.js | 3 - .../plugins/umbracocss/langs/ja_dlg.js | 3 - .../tinymce/plugins/umbracocss/langs/ru.js | 3 - .../plugins/umbracocss/langs/ru_dlg.js | 3 - .../tinymce/plugins/umbracocss/langs/sv.js | 3 - .../plugins/umbracocss/langs/sv_dlg.js | 3 - .../tinymce/plugins/umbracocss/langs/zh.js | 3 - .../plugins/umbracocss/langs/zh_dlg.js | 3 - .../tinymce/plugins/umbracocss/plugin.min.js | 182 ------- .../tinymce/plugins/umbracoembed/dialog.htm | 92 ---- .../plugins/umbracoembed/editor_plugin.js | 1 - .../plugins/umbracoembed/editor_plugin_src.js | 84 ---- .../plugins/umbracoembed/img/ajax-loader.gif | Bin 1928 -> 0 bytes .../plugins/umbracoembed/img/embed.gif | Bin 260 -> 0 bytes .../plugins/umbracoembed/img/embed.png | Bin 537 -> 0 bytes .../tinymce/plugins/umbracoembed/js/dialog.js | 90 ---- .../tinymce/plugins/umbracoembed/langs/da.js | 3 - .../plugins/umbracoembed/langs/da_dlg.js | 9 - .../tinymce/plugins/umbracoembed/langs/de.js | 10 - .../plugins/umbracoembed/langs/de_dlg.js | 10 - .../tinymce/plugins/umbracoembed/langs/en.js | 3 - .../plugins/umbracoembed/langs/en_dlg.js | 10 - .../plugins/umbracoembed/langs/en_us.js | 3 - .../plugins/umbracoembed/langs/en_us_dlg.js | 10 - .../tinymce/plugins/umbracoembed/langs/it.js | 9 - .../plugins/umbracoembed/langs/it_dlg.js | 9 - .../tinymce/plugins/umbracoembed/langs/ja.js | 10 - .../plugins/umbracoembed/langs/ja_dlg.js | 10 - .../tinymce/plugins/umbracoembed/langs/ru.js | 10 - .../plugins/umbracoembed/langs/ru_dlg.js | 10 - .../tinymce/plugins/umbracoembed/langs/sv.js | 10 - .../plugins/umbracoembed/langs/sv_dlg.js | 10 - .../tinymce/plugins/umbracoembed/langs/zh.js | 10 - .../plugins/umbracoembed/langs/zh_dlg.js | 9 - .../plugins/umbracoimg/editor_plugin_src.js | 52 -- .../tinymce/plugins/umbracoimg/js/image.js | 332 ------------- .../plugins/umbracoimg/langs/en_dlg.js | 43 -- .../plugins/umbracoimg/langs/en_us_dlg.js | 43 -- .../plugins/umbracoimg/langs/he_dlg.js | 43 -- .../plugins/umbracoimg/langs/it_dlg.js | 43 -- .../plugins/umbracoimg/langs/ja_dlg.js | 43 -- .../plugins/umbracoimg/langs/ru_dlg.js | 43 -- .../plugins/umbracoimg/langs/sv_dlg.js | 43 -- .../plugins/umbracoimg/langs/zh_dlg.js | 43 -- .../tinymce/plugins/umbracolink/plugin.min.js | 5 - .../tinymce/plugins/umbracomacro/dialog.htm | 27 -- .../plugins/umbracomacro/editor_plugin_src.js | 145 ------ .../plugins/umbracomacro/img/insMacro.gif | Bin 603 -> 0 bytes .../tinymce/plugins/umbracomacro/js/dialog.js | 19 - .../tinymce/plugins/umbracomacro/langs/en.js | 3 - .../plugins/umbracomacro/langs/en_dlg.js | 3 - .../plugins/umbracomacro/langs/en_us.js | 3 - .../plugins/umbracomacro/langs/en_us_dlg.js | 3 - .../tinymce/plugins/umbracomacro/langs/he.js | 3 - .../plugins/umbracomacro/langs/he_dlg.js | 3 - .../tinymce/plugins/umbracomacro/langs/ja.js | 3 - .../plugins/umbracomacro/langs/ja_dlg.js | 3 - .../tinymce/plugins/umbracomacro/langs/ru.js | 3 - .../plugins/umbracomacro/langs/ru_dlg.js | 3 - .../tinymce/plugins/umbracomacro/langs/sv.js | 3 - .../plugins/umbracomacro/langs/sv_dlg.js | 3 - .../tinymce/plugins/umbracomacro/langs/zh.js | 3 - .../plugins/umbracomacro/langs/zh_dlg.js | 3 - .../plugins/umbracopaste/editor_plugin_src.js | 53 -- .../umbracoshortcut/editor_plugin_src.js | 43 -- .../components/grid/grid.rte.directive.js | 2 +- .../propertyeditors/rte/rte.controller.js | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 93 ---- .../Umbraco/create/PartialView.ascx | 31 -- .../Umbraco/create/PartialView.ascx.cs | 67 --- .../create/PartialView.ascx.designer.cs | 69 --- src/Umbraco.Web.UI/packages.config | 1 - src/Umbraco.Web.UI/umbraco/channels/rsd.aspx | 20 - .../umbraco/channels/wlwmanifest.aspx | 51 -- .../umbraco/config/create/UI.Release.xml | 73 --- .../umbraco/config/create/UI.xml | 74 +-- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 + .../umbraco/config/lang/en_us.xml | 2 + .../umbraco/controls/Images/ImageViewer.ascx | 49 -- .../controls/Images/ImageViewer.ascx.cs | 9 - .../Images/ImageViewer.ascx.designer.cs | 15 - .../umbraco/controls/Images/ImageViewer.js | 133 ----- .../controls/Images/ImageViewerUpdater.asmx | 1 - .../controls/Images/UploadMediaImage.ascx | 28 -- .../controls/Images/UploadMediaImage.js | 52 -- .../umbraco/controls/PasswordChanger.ascx.cs | 35 -- .../controls/PasswordChanger.ascx.designer.cs | 69 --- .../umbraco/controls/passwordChanger.ascx | 133 ----- .../umbraco/create/DLRScripting.ascx | 40 -- .../umbraco/create/DlrScripting.ascx.cs | 11 - .../create/DlrScripting.ascx.designer.cs | 15 - .../umbraco/create/PartialViewMacro.ascx | 38 -- .../umbraco/create/PartialViewMacro.ascx.cs | 82 ---- .../create/PartialViewMacro.ascx.designer.cs | 87 ---- src/Umbraco.Web.UI/umbraco/create/User.ascx | 35 -- .../umbraco/create/User.ascx.cs | 88 ---- .../umbraco/create/User.ascx.designer.cs | 105 ---- src/Umbraco.Web.UI/umbraco/create/script.ascx | 34 -- .../umbraco/css/permissionsEditor.css | 43 -- src/Umbraco.Web.UI/umbraco/css/umbracoGui.css | 455 ------------------ .../umbraco/dashboard/ExamineManagement.ascx | 216 --------- .../dashboard/ExamineManagement.ascx.cs | 14 - .../ExamineManagement.ascx.designer.cs | 60 --- .../umbraco/dashboard/LatestEdits.ascx | 11 - .../umbraco/dialogs/AssignDomain.aspx | 47 -- .../umbraco/dialogs/MoveOrCopy.aspx.cs | 11 - .../dialogs/MoveOrCopy.aspx.designer.cs | 15 - .../umbraco/dialogs/SendPublish.aspx | 31 +- src/Umbraco.Web.UI/umbraco/dialogs/about.aspx | 33 -- src/Umbraco.Web.UI/umbraco/dialogs/cruds.aspx | 25 - .../umbraco/dialogs/emptyTrashcan.aspx | 83 ---- .../umbraco/dialogs/imageViewer.aspx | 14 - .../umbraco/dialogs/insertMacro.aspx | 103 ---- .../dialogs/insertMasterpageContent.aspx | 32 -- .../dialogs/insertMasterpagePlaceholder.aspx | 36 -- .../umbraco/dialogs/insertTable.aspx | 290 ----------- .../umbraco/dialogs/mediaPicker.aspx | 97 ---- .../umbraco/dialogs/moveOrCopy.aspx | 92 ---- .../umbraco/dialogs/rollBack.aspx | 2 +- .../umbraco/dialogs/uploadImage.aspx | 28 -- .../TheOutfield/DesktopMediaUploader/dmu.ashx | 1 - .../plugins/tinymce3/InsertAnchor.aspx | 41 -- .../umbraco/plugins/tinymce3/insertChar.aspx | 64 --- .../umbraco/plugins/tinymce3/insertImage.aspx | 202 -------- .../umbraco/plugins/tinymce3/insertLink.aspx | 149 ------ .../umbraco/plugins/tinymce3/insertMacro.aspx | 160 ------ .../Application/UmbracoApplicationActions.js | 47 +- .../Trees/LegacyTreeDataConverter.cs | 23 +- src/Umbraco.Web/Umbraco.Web.csproj | 161 +------ .../umbraco/Trees/loadTemplates.cs | 2 + .../controls/Images/ImageViewerUpdater.asmx | 1 - .../Images/ImageViewerUpdater.asmx.cs | 73 --- .../controls/Images/UploadMediaImage.ascx | 28 -- .../controls/Images/UploadMediaImage.ascx.cs | 115 ----- .../Images/UploadMediaImage.ascx.designer.cs | 109 ----- .../umbraco/controls/passwordChanger.ascx.cs | 151 ------ .../umbraco/create/DLRScripting.ascx.cs | 185 ------- .../umbraco/create/DLRScriptingTasks.cs | 97 ---- .../umbraco/create/ScriptTasks.cs | 76 --- .../umbraco/create/script.ascx.cs | 125 ----- .../umbraco/create/templateTasks.cs | 57 --- .../umbraco/dashboard/LatestEdits.ascx | 14 - .../umbraco/dashboard/LatestEdits.ascx.cs | 68 --- .../dashboard/LatestEdits.ascx.designer.cs | 24 - .../MediaDashboardFolderBrowser.ascx | 4 - .../umbraco/dialogs/AssignDomain.aspx.cs | 270 ----------- .../umbraco/dialogs/about.aspx | 33 -- .../umbraco/dialogs/about.aspx.cs | 41 -- .../umbraco/dialogs/about.aspx.designer.cs | 33 -- .../umbraco/dialogs/cruds.aspx.cs | 146 ------ .../umbraco/dialogs/emptyTrashcan.aspx.cs | 66 --- .../umbraco/dialogs/imageViewer.aspx | 14 - .../umbraco/dialogs/imageViewer.aspx.cs | 93 ---- .../dialogs/imageViewer.aspx.designer.cs | 34 -- .../umbraco/dialogs/insertMacro.aspx | 102 ---- .../umbraco/dialogs/insertMacro.aspx.cs | 125 ----- .../dialogs/insertMacro.aspx.designer.cs | 51 -- .../umbraco/dialogs/insertTable.aspx | 290 ----------- .../umbraco/dialogs/insertTable.aspx.cs | 62 --- .../dialogs/insertTable.aspx.designer.cs | 33 -- .../umbraco/dialogs/mediaPicker.aspx | 97 ---- .../umbraco/dialogs/mediaPicker.aspx.cs | 33 -- .../dialogs/mediaPicker.aspx.designer.cs | 79 --- .../umbraco/dialogs/uploadImage.aspx | 28 -- .../umbraco/dialogs/uploadImage.aspx.cs | 26 - .../dialogs/uploadImage.aspx.designer.cs | 25 - .../plugins/tinymce3/InsertAnchor.aspx | 41 -- .../plugins/tinymce3/InsertAnchor.aspx.cs | 30 -- .../tinymce3/InsertAnchor.aspx.designer.cs | 42 -- .../umbraco/plugins/tinymce3/SpellChecker.cs | 29 -- .../plugins/tinymce3/SpellCheckerInput.cs | 80 --- .../plugins/tinymce3/SpellCheckerResult.cs | 35 -- .../umbraco/plugins/tinymce3/insertChar.aspx | 64 --- .../plugins/tinymce3/insertChar.aspx.cs | 28 -- .../tinymce3/insertChar.aspx.designer.cs | 42 -- .../umbraco/plugins/tinymce3/insertImage.aspx | 202 -------- .../plugins/tinymce3/insertImage.aspx.cs | 57 --- .../tinymce3/insertImage.aspx.designer.cs | 222 --------- .../umbraco/plugins/tinymce3/insertLink.aspx | 148 ------ .../plugins/tinymce3/insertLink.aspx.cs | 64 --- .../tinymce3/insertLink.aspx.designer.cs | 150 ------ .../umbraco/plugins/tinymce3/insertMacro.aspx | 159 ------ .../plugins/tinymce3/insertMacro.aspx.cs | 230 --------- .../tinymce3/insertMacro.aspx.designer.cs | 177 ------- .../umbraco/webservices/MediaUploader.ashx | 1 - .../umbraco/webservices/MediaUploader.ashx.cs | 399 --------------- 197 files changed, 38 insertions(+), 10972 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocontextmenu/editor_plugin_src.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocontextmenu/plugin.min.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/img/example.gif delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/js/dialog.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/en.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/en_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/en_us.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/en_us_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/it.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/it_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/ja.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/ja_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/ru.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/ru_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/sv.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/sv_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/zh.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/langs/zh_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/plugin.min.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/dialog.htm delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/editor_plugin.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/editor_plugin_src.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/img/ajax-loader.gif delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/img/embed.gif delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/img/embed.png delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/js/dialog.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/da.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/da_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/de.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/de_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_us.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_us_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/it.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/it_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ja.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ja_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ru.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ru_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/sv.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/sv_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/zh.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/zh_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/editor_plugin_src.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/js/image.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/en_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/en_us_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/he_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/it_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/ja_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/ru_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/sv_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/zh_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracolink/plugin.min.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/dialog.htm delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/editor_plugin_src.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/img/insMacro.gif delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/js/dialog.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_us.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_us_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/he.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/he_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ja.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ja_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ru.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ru_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/sv.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/sv_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/zh.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/zh_dlg.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracopaste/editor_plugin_src.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoshortcut/editor_plugin_src.js delete mode 100644 src/Umbraco.Web.UI/Umbraco/create/PartialView.ascx delete mode 100644 src/Umbraco.Web.UI/Umbraco/create/PartialView.ascx.cs delete mode 100644 src/Umbraco.Web.UI/Umbraco/create/PartialView.ascx.designer.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/channels/rsd.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/channels/wlwmanifest.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewer.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewer.ascx.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewer.ascx.designer.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewer.js delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewerUpdater.asmx delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/Images/UploadMediaImage.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/Images/UploadMediaImage.js delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/PasswordChanger.ascx.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/PasswordChanger.ascx.designer.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/create/DLRScripting.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/create/DlrScripting.ascx.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/create/DlrScripting.ascx.designer.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/create/PartialViewMacro.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/create/PartialViewMacro.ascx.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/create/PartialViewMacro.ascx.designer.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/create/User.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/create/User.ascx.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/create/User.ascx.designer.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/create/script.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/css/permissionsEditor.css delete mode 100644 src/Umbraco.Web.UI/umbraco/css/umbracoGui.css delete mode 100644 src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx.designer.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/dashboard/LatestEdits.ascx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/AssignDomain.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/MoveOrCopy.aspx.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/MoveOrCopy.aspx.designer.cs delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/about.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/cruds.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/emptyTrashcan.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/imageViewer.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/insertMacro.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/insertMasterpageContent.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/insertMasterpagePlaceholder.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/insertTable.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/mediaPicker.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/moveOrCopy.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/dialogs/uploadImage.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/plugins/TheOutfield/DesktopMediaUploader/dmu.ashx delete mode 100644 src/Umbraco.Web.UI/umbraco/plugins/tinymce3/InsertAnchor.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/plugins/tinymce3/insertChar.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/plugins/tinymce3/insertImage.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/plugins/tinymce3/insertLink.aspx delete mode 100644 src/Umbraco.Web.UI/umbraco/plugins/tinymce3/insertMacro.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewerUpdater.asmx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewerUpdater.asmx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/UploadMediaImage.ascx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/UploadMediaImage.ascx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/UploadMediaImage.ascx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/controls/passwordChanger.ascx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/create/DLRScripting.ascx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/create/DLRScriptingTasks.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/create/ScriptTasks.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/create/script.ascx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/LatestEdits.ascx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/LatestEdits.ascx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/LatestEdits.ascx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/MediaDashboardFolderBrowser.ascx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/about.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/about.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/about.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/cruds.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/emptyTrashcan.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/imageViewer.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/imageViewer.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/imageViewer.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/insertMacro.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/insertMacro.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/insertMacro.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/insertTable.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/insertTable.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/insertTable.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/mediaPicker.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/mediaPicker.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/mediaPicker.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/uploadImage.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/uploadImage.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/uploadImage.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/InsertAnchor.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/InsertAnchor.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/InsertAnchor.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/SpellChecker.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/SpellCheckerInput.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/SpellCheckerResult.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertChar.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertChar.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertChar.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertImage.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertImage.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertImage.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertLink.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertLink.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertLink.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertMacro.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertMacro.aspx.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/plugins/tinymce3/insertMacro.aspx.designer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/webservices/MediaUploader.ashx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/webservices/MediaUploader.ashx.cs diff --git a/src/Umbraco.Tests/UI/LegacyDialogTests.cs b/src/Umbraco.Tests/UI/LegacyDialogTests.cs index 709945d88d..e7749cf80b 100644 --- a/src/Umbraco.Tests/UI/LegacyDialogTests.cs +++ b/src/Umbraco.Tests/UI/LegacyDialogTests.cs @@ -27,15 +27,12 @@ namespace Umbraco.Tests.UI } [TestCase(typeof(XsltTasks), DefaultApps.developer)] - [TestCase(typeof(templateTasks), DefaultApps.settings)] [TestCase(typeof(StylesheetTasks), DefaultApps.settings)] [TestCase(typeof(stylesheetPropertyTasks), DefaultApps.settings)] - [TestCase(typeof(ScriptTasks), DefaultApps.settings)] [TestCase(typeof(MemberGroupTasks), DefaultApps.member)] [TestCase(typeof(dictionaryTasks), DefaultApps.settings)] [TestCase(typeof(macroTasks), DefaultApps.developer)] [TestCase(typeof(languageTasks), DefaultApps.settings)] - [TestCase(typeof(DLRScriptingTasks), DefaultApps.developer)] [TestCase(typeof(CreatedPackageTasks), DefaultApps.developer)] public void Check_Assigned_Apps_For_Tasks(Type taskType, DefaultApps app) { diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocontextmenu/editor_plugin_src.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocontextmenu/editor_plugin_src.js deleted file mode 100644 index f3074ef21f..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocontextmenu/editor_plugin_src.js +++ /dev/null @@ -1,69 +0,0 @@ -/** -* editor_plugin_src.js -* -* Copyright 2012, Umbraco -* Released under MIT License. -* -* License: http://opensource.org/licenses/mit-license.html -*/ - -(function () { - var Event = tinymce.dom.Event, each = tinymce.each, DOM = tinymce.DOM; - - /** - * This plugin modifies the standard TinyMCE context menu, with umbraco specific changes. - * - * @class tinymce.plugins.umbContextMenu - */ - tinymce.create('tinymce.plugins.UmbracoContextMenu', { - /** - * Initializes the plugin, this will be executed after the plugin has been created. - * This call is done before the editor instance has finished it's initialization so use the onInit event - * of the editor instance to intercept that event. - * - * @method init - * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in. - * @param {string} url Absolute URL to where the plugin is located. - */ - init: function (ed) { - if (ed.plugins.contextmenu) { - - ed.plugins.contextmenu.onContextMenu.add(function (th, menu, event) { - - var keys = UmbClientMgr.uiKeys(); - - $.each(menu.items, function (idx, el) { - - switch (el.settings.cmd) { - case "Cut": - el.settings.title = keys['defaultdialogs_cut']; - break; - case "Copy": - el.settings.title = keys['general_copy']; - break; - case "Paste": - el.settings.title = keys['defaultdialogs_paste']; - break; - case "mceAdvLink": - case "mceLink": - el.settings.title = keys['defaultdialogs_insertlink']; - break; - case "UnLink": - el.settings.title = keys['relatedlinks_removeLink']; - break; - case "mceImage": - el.settings.title = keys['defaultdialogs_insertimage']; - el.settings.cmd = "mceUmbimage"; - break; - } - - }); - - }); - } - } - }); - - // Register plugin - tinymce.PluginManager.add('umbracocontextmenu', tinymce.plugins.UmbracoContextMenu); -})(); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocontextmenu/plugin.min.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocontextmenu/plugin.min.js deleted file mode 100644 index f3074ef21f..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocontextmenu/plugin.min.js +++ /dev/null @@ -1,69 +0,0 @@ -/** -* editor_plugin_src.js -* -* Copyright 2012, Umbraco -* Released under MIT License. -* -* License: http://opensource.org/licenses/mit-license.html -*/ - -(function () { - var Event = tinymce.dom.Event, each = tinymce.each, DOM = tinymce.DOM; - - /** - * This plugin modifies the standard TinyMCE context menu, with umbraco specific changes. - * - * @class tinymce.plugins.umbContextMenu - */ - tinymce.create('tinymce.plugins.UmbracoContextMenu', { - /** - * Initializes the plugin, this will be executed after the plugin has been created. - * This call is done before the editor instance has finished it's initialization so use the onInit event - * of the editor instance to intercept that event. - * - * @method init - * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in. - * @param {string} url Absolute URL to where the plugin is located. - */ - init: function (ed) { - if (ed.plugins.contextmenu) { - - ed.plugins.contextmenu.onContextMenu.add(function (th, menu, event) { - - var keys = UmbClientMgr.uiKeys(); - - $.each(menu.items, function (idx, el) { - - switch (el.settings.cmd) { - case "Cut": - el.settings.title = keys['defaultdialogs_cut']; - break; - case "Copy": - el.settings.title = keys['general_copy']; - break; - case "Paste": - el.settings.title = keys['defaultdialogs_paste']; - break; - case "mceAdvLink": - case "mceLink": - el.settings.title = keys['defaultdialogs_insertlink']; - break; - case "UnLink": - el.settings.title = keys['relatedlinks_removeLink']; - break; - case "mceImage": - el.settings.title = keys['defaultdialogs_insertimage']; - el.settings.cmd = "mceUmbimage"; - break; - } - - }); - - }); - } - } - }); - - // Register plugin - tinymce.PluginManager.add('umbracocontextmenu', tinymce.plugins.UmbracoContextMenu); -})(); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/img/example.gif b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracocss/img/example.gif deleted file mode 100644 index 1ab5da4461113d2af579898528246fdbe52ecd00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87 zcmZ?wbhEHb6k!lyn83&Y1dNP~ia%L^OhyJB5FaGNz@*pGzw+SQ`#f{}FJ-?!v#V)e mtsGNfpJeCKSAiOz**>0`XR2{OVa>-G_df0vaY 1) { - // register with new formatter engine (can't access from here so a hack in the set style above!) - // tinyMCE.activeEditor.formatter.register('umb' + alias.substring(1, alias.length), { - // classes: alias.substring(1, alias.length) - // }); - } - styles.add(name, alias); - } - } - - - return styles; - } - - return null; - }, - - - /** - * Returns information about the plugin as a name/value array. - * The current keys are longname, author, authorurl, infourl and version. - * - * @return {Object} Name/value array containing information about the plugin. - */ - getInfo: function () { - return { - longname: 'Umbraco CSS/Styling Plugin', - author: 'Umbraco', - authorurl: 'http://umbraco.org', - infourl: 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/example', - version: "1.0" - }; - } - }); - - // Register plugin - tinymce.PluginManager.add('umbracocss', tinymce.plugins.umbracocss); -})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/dialog.htm b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/dialog.htm deleted file mode 100644 index a89fcc1283..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/dialog.htm +++ /dev/null @@ -1,92 +0,0 @@ - - - - {#embed_dlg.title} - - - - - - - - -
- -
-
-
- {#embed_dlg.general} - - - - - - - - - -
- -
- - - - - - -
x   
-
- -
-
- {#embed_dlg.preview} -
-
- -
-
-
-
-
-
- {#embed_dlg.source} - -
-
-
- - -
- - -
-
- - - diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/editor_plugin.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/editor_plugin.js deleted file mode 100644 index ec1f81ea40..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/editor_plugin.js +++ /dev/null @@ -1 +0,0 @@ -(function(){tinymce.PluginManager.requireLangPack("example");tinymce.create("tinymce.plugins.ExamplePlugin",{init:function(a,b){a.addCommand("mceExample",function(){a.windowManager.open({file:b+"/dialog.htm",width:320+parseInt(a.getLang("example.delta_width",0)),height:120+parseInt(a.getLang("example.delta_height",0)),inline:1},{plugin_url:b,some_custom_arg:"custom arg"})});a.addButton("example",{title:"example.desc",cmd:"mceExample",image:b+"/img/example.gif"});a.onNodeChange.add(function(d,c,e){c.setActive("example",e.nodeName=="IMG")})},createControl:function(b,a){return null},getInfo:function(){return{longname:"Example plugin",author:"Some author",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/example",version:"1.0"}}});tinymce.PluginManager.add("example",tinymce.plugins.ExamplePlugin)})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/editor_plugin_src.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/editor_plugin_src.js deleted file mode 100644 index 4649f37ecf..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/editor_plugin_src.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * editor_plugin_src.js - * - * Copyright 2009, Moxiecode Systems AB - * Released under LGPL License. - * - * License: http://tinymce.moxiecode.com/license - * Contributing: http://tinymce.moxiecode.com/contributing - */ - -(function() { - // Load plugin specific language pack - tinymce.PluginManager.requireLangPack('umbracoembed'); - - tinymce.create('tinymce.plugins.umbracoembed', { - /** - * Initializes the plugin, this will be executed after the plugin has been created. - * This call is done before the editor instance has finished it's initialization so use the onInit event - * of the editor instance to intercept that event. - * - * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in. - * @param {string} url Absolute URL to where the plugin is located. - */ - init : function(ed, url) { - // Register the command so that it can be invoked by using tinyMCE.activeEditor.execCommand('mceExample'); - ed.addCommand('mceUmbracoEmbed', function() { - ed.windowManager.open({ - file : url + '/dialog.htm', - width : 600 + parseInt(ed.getLang('example.delta_width', 0)), - height : 400 + parseInt(ed.getLang('example.delta_height', 0)), - inline : 1 - }, { - plugin_url : url, // Plugin absolute URL - some_custom_arg : 'custom arg' // Custom argument - }); - }); - - // Register example button - ed.addButton('umbracoembed', { - title : 'umbracoembed.desc', - cmd : 'mceUmbracoEmbed', - image : url + '/img/embed.gif' - }); - - // Add a node change handler, selects the button in the UI when a image is selected - /*ed.onNodeChange.add(function(ed, cm, n) { - cm.setActive('example', n.nodeName == 'IMG'); - });*/ - }, - - /** - * Creates control instances based in the incomming name. This method is normally not - * needed since the addButton method of the tinymce.Editor class is a more easy way of adding buttons - * but you sometimes need to create more complex controls like listboxes, split buttons etc then this - * method can be used to create those. - * - * @param {String} n Name of the control to create. - * @param {tinymce.ControlManager} cm Control manager to use inorder to create new control. - * @return {tinymce.ui.Control} New control instance or null if no control was created. - */ - createControl : function(n, cm) { - return null; - }, - - /** - * Returns information about the plugin as a name/value array. - * The current keys are longname, author, authorurl, infourl and version. - * - * @return {Object} Name/value array containing information about the plugin. - */ - getInfo : function() { - return { - longname : 'Umbraco Embed', - author : 'Tim Geyssens', - authorurl : 'http://http://umbraco.com/', - infourl : 'http://http://umbraco.com/', - version : "1.0" - }; - } - }); - - // Register plugin - tinymce.PluginManager.add('umbracoembed', tinymce.plugins.umbracoembed); -})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/img/ajax-loader.gif b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/img/ajax-loader.gif deleted file mode 100644 index 521a291d748c84b37cf75fe7003ca0ff04f33fb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1928 zcmdVa?@tz=)H2#yZ97W^J8C-PQkOKwEW@eMrL&ze+4jWP zV^YDJfAZl`ssWC@PoBeLmm#__$Cg ztgWrx+1V*BF3!x%EGsMP>+3@hq_eZr@At2)tZ=#9hK2?l$A^c9o12?kTU#qDD+7Um zLZMh%S{fJ_u-R<&_4OK!rm(P3B9Rmn6u8}PtybIA)Kp$xUQ<)!bUFnB!TS1oV`HPs zn98)6rvP@y%!q zMV&DtHhafuf;rg1LChe_Wrd{#ZNmBv0YI}NH$b0A>2g~JlT#OgmBF-MmtE=N#f6EC zMaC>U(GB_rCE4z$vt9z>Zh8omx7kndovud@Kj?PWx3Lm`e)!R7vsnFz2j$#+a(%E= z6fTI0>CN*;VWpM@HMKcTD-1@J!7Uj5&7lc{BL?6w^HVimm8!a$mFaTn3c) zx+3)(5qm9L6Du8m`Qp|3Fe_0}b-Vl7eKQSo)@kC9uo{e1+m5T*`NxO+ubTf>9rHaN zRNF5G(L-nTAZwHoillnlWXJRj4$ZOWP4vlSv&He8SaopH2+TWq=`!J2ymHROSR|or z1NhrkK`K77MKVL^OGcEp#am$AXf-@gtJJa5(U+8M9{(JZRZO>vZ$V(9Otof~#)R3l zNOu4K*umZSZ`iOr%uYJaPU_MJ-OQkS%pr@%Xj4O37&OLz2PF zCIFu!{i@1qdOjz-!YnlY|di&S4|F-Jwp z$El=B_lG1t=g!ymdr@lRO}h4WjK7~6QH2Yj$bxT=7YMmfLj2LXz|6PXO>&~cgGWQ_ zV&@?EQ2`4K6(j@;91uqL*m;=6+ap-bgjtr{_Fz+j0%(Kk>nFObU3o$w|e4FHR$QNaKJ diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/img/embed.png b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/img/embed.png deleted file mode 100644 index a8d147d76cd81bb242538ee22760db471a28998b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537 zcmV+!0_OdRP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXP; z6%!Cz#+@<%00EgvL_t(I%k7jsOCwPfhM!2XsH8|BYNa?@3^qX#gM}~_u?RMzh+sDs zasR+p+oT+~@()PZW{WArM#a)jP%upj323kp#bQK_;wbMni5rtyMX^xu>Fzm)d(VB( z2Lxbv(}N-yxsIN3*HcVJRZK=D*AZawI{aVX1)!Cm|6bGVumG+m2gLg#*O6~qIRM|b zBo$Yt?nm|`=v$QE>1M(v)6}!IKV~^%Wn|jV^A-LZ(F%)MIUJa@TLb9 z9vkEO;sSt3e*5=*xlDa<3qWf}2M_1x#8wNTs{QC>eI0gpY1G!ZnjEM#5XEFvnjKa# z84WDHQ56{=UKRg~z+TU$rEEGEV9zO={;zPMMnh$ORBcmJkLR1UHQN8|gkJJBU3i)? z)oI&ME_3I3c>DWwZ65RiM~!7Z&6tO=p&`D-V$j>mSw2tK=0Q_(Mk$q0Wo|@T7?)f} zrBtHnYo$~|avf=5T$Q4;abzN)gNB2_q}%FjoP|sApU7r b2LOKoGp)W|>}K$200000NkvXXu0mjf'); - $('#source').val(''); - - $.ajax({ - type: 'POST', - async: true, - url: '../../../../base/EmbedMediaService/Embed/', - data: { url: url, width: width, height: height }, - dataType: 'json', - success: function (result) { - switch (result.Status) { - case 0: - //not supported - $('#preview').html('Not Supported'); - break; - case 1: - //error - $('#preview').html('Error'); - break; - case 2: - $('#preview').html(result.Markup); - $('#source').val(result.Markup); - if (result.SupportsDimensions) { - $('#dimensions').show(); - } else { - $('#dimensions').hide(); - } - $('#insert').removeAttr('disabled'); - break; - } - }, - error: function (xhr, ajaxOptions, thrownError) { - $('#preview').html("Error"); - } - }); - }, - beforeResize: function () { - this.width = parseInt($('#width').val(), 10); - this.height = parseInt($('#height').val(), 10); - }, - changeSize: function (type) { - var width, height, scale, size; - - if ($('#constrain').is(':checked')) { - width = parseInt($('#width').val(), 10); - height = parseInt($('#height').val(), 10); - if (type == 'width') { - this.height = Math.round((width / this.width) * height); - $('#height').val(this.height); - } else { - this.width = Math.round((height / this.height) * width); - $('#width').val(this.width); - } - } - if ($('#url').val() != '') { - UmbracoEmbedDialog.showPreview(); - } - }, - changeSource: function (type) { - if ($('#source').val() != '') { - $('#insert').removeAttr('disabled'); - } - else { - $('#insert').attr('disabled', 'disabled'); - } - }, - updatePreviewFromSource: function (type) { - var sourceVal = $('#source').val(); - - if (sourceVal != '') { - $('#preview').html(sourceVal); - } - } -}; - diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/da.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/da.js deleted file mode 100644 index a93d2e36f2..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/da.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('da.umbracoembed', { - desc: 'Inds\u00E6t ekstern mediefil' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/da_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/da_dlg.js deleted file mode 100644 index 3082589539..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/da_dlg.js +++ /dev/null @@ -1,9 +0,0 @@ -tinyMCE.addI18n('da.embed_dlg', { - title: 'Inds\u00E6t ekstern mediefil', - general: 'Generelt', - url: 'Url:', - size: 'Dimensioner:', - constrain_proportions: 'Bevar proportioner', - preview: 'Vis', - source: 'Vis kilde' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/de.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/de.js deleted file mode 100644 index ad0b940580..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/de.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('de.embed_dlg', { - title: 'Medien von Drittanbietern einbetten', - general: 'Allgemein', - url: 'Url:', - size: 'Abmessungen:', - constrain_proportions: 'Proportionen beibehalten', - preview: 'Vorschau', - source: 'Quellcode' - -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/de_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/de_dlg.js deleted file mode 100644 index ad0b940580..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/de_dlg.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('de.embed_dlg', { - title: 'Medien von Drittanbietern einbetten', - general: 'Allgemein', - url: 'Url:', - size: 'Abmessungen:', - constrain_proportions: 'Proportionen beibehalten', - preview: 'Vorschau', - source: 'Quellcode' - -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en.js deleted file mode 100644 index 2b086df34e..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('en.umbracoembed', { - desc: 'Embed third party media' -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_dlg.js deleted file mode 100644 index e131d87533..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_dlg.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('en.embed_dlg', { - title: 'Embed third party media', - general: 'General', - url: 'Url:', - size: 'Size:', - constrain_proportions: 'Constrain', - preview: 'Preview', - source: 'Source' - -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_us.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_us.js deleted file mode 100644 index 4698979aab..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_us.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('en_us.umbracoembed', { - desc: 'Embed third party media' -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_us_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_us_dlg.js deleted file mode 100644 index 8c6a070226..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/en_us_dlg.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('en_us.embed_dlg', { - title: 'Embed third party media', - general: 'General', - url: 'Url:', - size: 'Size:', - constrain_proportions: 'Constrain', - preview: 'Preview', - source: 'Source' - -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/it.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/it.js deleted file mode 100644 index a8ff6693ab..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/it.js +++ /dev/null @@ -1,9 +0,0 @@ -tinyMCE.addI18n('en.embed_dlg', { -title: 'Integra media di terze parti', -general: 'Generale', -url: 'Url:', -size: 'Dimensione:', -constrain_proportions: 'Vincolo', -preview: 'Anteprima', -source: 'Sorgente' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/it_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/it_dlg.js deleted file mode 100644 index 87766ce2da..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/it_dlg.js +++ /dev/null @@ -1,9 +0,0 @@ -tinyMCE.addI18n('it.embed_dlg', { -title: 'Integra media di terze parti', -general: 'Generale', -url: 'Url:', -size: 'Dimensione:', -constrain_proportions: 'Vincolo', -preview: 'Anteprima', -source: 'Sorgente' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ja.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ja.js deleted file mode 100644 index c525d3a37e..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ja.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('ja.embed_dlg', { - title: 'サードパーティメディアの埋め込み', - general: '一般', - url: 'Url:', - size: 'サイズ:', - constrain_proportions: '制約', - preview: 'プレビュー', - source: 'ソース' - -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ja_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ja_dlg.js deleted file mode 100644 index db01d093c9..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ja_dlg.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('ja.embed_dlg', { - title: サードパーティメディアの埋め込み', - general: '一般', - url: 'Url:', - size: 'サイズ:', - constrain_proportions: '制約', - preview: 'プレビュー', - source: 'ソース' - -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ru.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ru.js deleted file mode 100644 index a1b566597b..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ru.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('ru.embed_dlg', { - title: 'Вставить внеший элемент медиа', - general: 'Общее', - url: 'Ссылка:', - size: 'Размер:', - constrain_proportions: 'Сохранять пропорции', - preview: 'Просмотр', - source: 'Источник' - -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ru_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ru_dlg.js deleted file mode 100644 index a1b566597b..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/ru_dlg.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('ru.embed_dlg', { - title: 'Вставить внеший элемент медиа', - general: 'Общее', - url: 'Ссылка:', - size: 'Размер:', - constrain_proportions: 'Сохранять пропорции', - preview: 'Просмотр', - source: 'Источник' - -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/sv.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/sv.js deleted file mode 100644 index 36bcaf8758..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/sv.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('sv.embed_dlg', { - title: 'Bädda in tredjeparts media', - general: 'Generell', - url: 'Url:', - size: 'Storlek:', - constrain_proportions: 'Bibehåll proportioner', - preview: 'Förhandsgranska', - source: 'Källa' - -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/sv_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/sv_dlg.js deleted file mode 100644 index 36bcaf8758..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/sv_dlg.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('sv.embed_dlg', { - title: 'Bädda in tredjeparts media', - general: 'Generell', - url: 'Url:', - size: 'Storlek:', - constrain_proportions: 'Bibehåll proportioner', - preview: 'Förhandsgranska', - source: 'Källa' - -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/zh.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/zh.js deleted file mode 100644 index ee41077410..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/zh.js +++ /dev/null @@ -1,10 +0,0 @@ -tinyMCE.addI18n('zh.embed_dlg', { - title: '嵌入第三方媒体', - general: '普通', - url: '链接:', - size: '尺寸:', - constrain_proportions: '约束比例', - preview: '预览', - source: '源' - -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/zh_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/zh_dlg.js deleted file mode 100644 index 2e59f0be58..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoembed/langs/zh_dlg.js +++ /dev/null @@ -1,9 +0,0 @@ -tinyMCE.addI18n('zh.embed_dlg', { - title: '嵌入第三方媒体', - general: '普通', - url: '链接:', - size: '尺寸:', - constrain_proportions: '约束比例', - preview: '预览', - source: '源' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/editor_plugin_src.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/editor_plugin_src.js deleted file mode 100644 index accb078909..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/editor_plugin_src.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * $Id: editor_plugin_src.js 677 2008-03-07 13:52:41Z spocke $ - * - * @author Moxiecode - * @copyright Copyright 2004-2008, Moxiecode Systems AB, All rights reserved. - */ - -(function() { -// tinymce.PluginManager.requireLangPack('umbraco'); - - tinymce.create('tinymce.plugins.UmbracoImagePlugin', { - init: function(ed, url) { - // Register commands - ed.addCommand('mceUmbimage', function() { - // Internal image object like a flash placeholder - if (ed.dom.getAttrib(ed.selection.getNode(), 'class').indexOf('mceItem') != -1) - return; - - ed.windowManager.open({ - /* UMBRACO SPECIFIC: Load Umbraco modal window */ - file: tinyMCE.activeEditor.getParam('umbraco_path') + '/plugins/tinymce3/insertImage.aspx', - width: 575 + ed.getLang('umbracoimg.delta_width', 0), - height: 505 + ed.getLang('umbracoimg.delta_height', 0), - inline: 1 - }, { - plugin_url: url - }); - }); - - // Register buttons - ed.addButton('image', { - title: 'advimage.image_desc', - cmd: 'mceUmbimage' - }); - - }, - - getInfo: function() { - return { - longname: 'Umbraco image dialog', - author: 'Umbraco', - authorurl: 'http://umbraco.org', - infourl: 'http://umbraco.org', - version: "1.0" - }; - } - }); - - // Register plugin - tinymce.PluginManager.add('umbracoimg', tinymce.plugins.UmbracoImagePlugin); - -})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/js/image.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/js/image.js deleted file mode 100644 index 25d0028fe8..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/js/image.js +++ /dev/null @@ -1,332 +0,0 @@ -var ImageDialog = { - preInit: function() { - var url; - - tinyMCEPopup.requireLangPack(); - - if (url = tinyMCEPopup.getParam("external_image_list_url")) - document.write(''); - }, - - init: function(ed) { - var f = document.forms[0], nl = f.elements, ed = tinyMCEPopup.editor, dom = ed.dom, n = ed.selection.getNode(); - - tinyMCEPopup.resizeToInnerSize(); - - if (n.nodeName == 'IMG') { - nl.src.value = dom.getAttrib(n, 'src'); - nl.width.value = dom.getAttrib(n, 'width'); - nl.height.value = dom.getAttrib(n, 'height'); - nl.alt.value = dom.getAttrib(n, 'alt'); - nl.orgHeight.value = dom.getAttrib(n, 'rel').split(",")[1]; - nl.orgWidth.value = dom.getAttrib(n, 'rel').split(",")[0]; - - } - - // If option enabled default contrain proportions to checked - if ((ed.getParam("advimage_constrain_proportions", true)) && f.constrain) - f.constrain.checked = true; - - this.changeAppearance(); - this.showPreviewImage(nl.src.value, 1); - }, - - insert: function(file, title) { - var ed = tinyMCEPopup.editor, t = this, f = document.forms[0]; - - if (f.src.value === '') { - if (ed.selection.getNode().nodeName == 'IMG') { - ed.dom.remove(ed.selection.getNode()); - ed.execCommand('mceRepaint'); - } - - tinyMCEPopup.close(); - return; - } - - if (tinyMCEPopup.getParam("accessibility_warnings", 1)) { - if (!f.alt.value) { - tinyMCEPopup.confirm(tinyMCEPopup.getLang('advimage_dlg.missing_alt'), function(s) { - if (s) - t.insertAndClose(); - }); - - return; - } - } - - t.insertAndClose(); - }, - - insertAndClose: function() { - var ed = tinyMCEPopup.editor, f = document.forms[0], nl = f.elements, v, args = {}, el; - - tinyMCEPopup.restoreSelection(); - - // Fixes crash in Safari - if (tinymce.isWebKit) - ed.getWin().focus(); - - if (!ed.settings.inline_styles) { - args = { - vspace: nl.vspace.value, - hspace: nl.hspace.value, - border: nl.border.value, - align: getSelectValue(f, 'align') - }; - } else { - // Remove deprecated values - args = { - vspace: '', - hspace: '', - border: '', - align: '' - }; - } - - tinymce.extend(args, { - src: nl.src.value, - width: nl.width.value, - height: nl.height.value, - alt: nl.alt.value, - title: nl.alt.value, - rel: nl.orgWidth.value + ',' + nl.orgHeight.value - }); - - args.onmouseover = args.onmouseout = ''; - - el = ed.selection.getNode(); - - if (el && el.nodeName == 'IMG') { - ed.dom.setAttribs(el, args); - } else { - ed.execCommand('mceInsertContent', false, '', { skip_undo: 1 }); - ed.dom.setAttribs('__mce_tmp', args); - ed.dom.setAttrib('__mce_tmp', 'id', ''); - ed.undoManager.add(); - } - - tinyMCEPopup.close(); - }, - - getAttrib: function(e, at) { - var ed = tinyMCEPopup.editor, dom = ed.dom, v, v2; - - if (ed.settings.inline_styles) { - switch (at) { - case 'align': - if (v = dom.getStyle(e, 'float')) - return v; - - if (v = dom.getStyle(e, 'vertical-align')) - return v; - - break; - - case 'hspace': - v = dom.getStyle(e, 'margin-left') - v2 = dom.getStyle(e, 'margin-right'); - - if (v && v == v2) - return parseInt(v.replace(/[^0-9]/g, '')); - - break; - - case 'vspace': - v = dom.getStyle(e, 'margin-top') - v2 = dom.getStyle(e, 'margin-bottom'); - if (v && v == v2) - return parseInt(v.replace(/[^0-9]/g, '')); - - break; - - case 'border': - v = 0; - - tinymce.each(['top', 'right', 'bottom', 'left'], function(sv) { - sv = dom.getStyle(e, 'border-' + sv + '-width'); - - // False or not the same as prev - if (!sv || (sv != v && v !== 0)) { - v = 0; - return false; - } - - if (sv) - v = sv; - }); - - if (v) - return parseInt(v.replace(/[^0-9]/g, '')); - - break; - } - } - - if (v = dom.getAttrib(e, at)) - return v; - - return ''; - }, - - setSwapImage: function(st) { - var f = document.forms[0]; - - f.onmousemovecheck.checked = st; - setBrowserDisabled('overbrowser', !st); - setBrowserDisabled('outbrowser', !st); - - if (f.over_list) - f.over_list.disabled = !st; - - if (f.out_list) - f.out_list.disabled = !st; - - f.onmouseoversrc.disabled = !st; - f.onmouseoutsrc.disabled = !st; - }, - - resetImageData: function() { - var f = document.forms[0]; - - f.elements.width.value = f.elements.height.value = ''; - }, - - updateImageData: function(img, st) { - var f = document.forms[0]; - - if (!st) { - f.elements.width.value = img.width; - f.elements.height.value = img.height; - } - - this.preloadImg = img; - }, - - changeAppearance: function() { - var ed = tinyMCEPopup.editor, f = document.forms[0], img = document.getElementById('alignSampleImg'); - - if (img) { - if (ed.getParam('inline_styles')) { - ed.dom.setAttrib(img, 'style', f.style.value); - } else { - img.align = f.align.value; - img.border = f.border.value; - img.hspace = f.hspace.value; - img.vspace = f.vspace.value; - } - } - }, - - changeHeight: function() { - var f = document.forms[0], tp, t = this; - alert(t.preloadImg); - - if (!f.constrain.checked || !t.preloadImg) { - return; - } - - if (f.width.value == '' || f.height.value == '') - return; - - tp = (parseInt(f.width.value) / parseInt(t.preloadImg.width)) * t.preloadImg.height; - f.height.value = tp.toFixed(0); - }, - - changeWidth: function() { - var f = document.forms[0], tp, t = this; - - if (!f.constrain.checked || !t.preloadImg) { - return; - } - - if (f.width.value == '' || f.height.value == '') - return; - - tp = (parseInt(f.height.value) / parseInt(t.preloadImg.height)) * t.preloadImg.width; - f.width.value = tp.toFixed(0); - }, - - updateStyle: function(ty) { - var dom = tinyMCEPopup.dom, st, v, f = document.forms[0], img = dom.create('img', { style: dom.get('style').value }); - - if (tinyMCEPopup.editor.settings.inline_styles) { - // Handle align - if (ty == 'align') { - dom.setStyle(img, 'float', ''); - dom.setStyle(img, 'vertical-align', ''); - - v = getSelectValue(f, 'align'); - if (v) { - if (v == 'left' || v == 'right') - dom.setStyle(img, 'float', v); - else - img.style.verticalAlign = v; - } - } - - // Handle border - if (ty == 'border') { - dom.setStyle(img, 'border', ''); - - v = f.border.value; - if (v || v == '0') { - if (v == '0') - img.style.border = '0'; - else - img.style.border = v + 'px solid black'; - } - } - - // Handle hspace - if (ty == 'hspace') { - dom.setStyle(img, 'marginLeft', ''); - dom.setStyle(img, 'marginRight', ''); - - v = f.hspace.value; - if (v) { - img.style.marginLeft = v + 'px'; - img.style.marginRight = v + 'px'; - } - } - - // Handle vspace - if (ty == 'vspace') { - dom.setStyle(img, 'marginTop', ''); - dom.setStyle(img, 'marginBottom', ''); - - v = f.vspace.value; - if (v) { - img.style.marginTop = v + 'px'; - img.style.marginBottom = v + 'px'; - } - } - - // Merge - dom.get('style').value = dom.serializeStyle(dom.parseStyle(img.style.cssText)); - } - }, - - changeMouseMove: function() { - }, - - showPreviewImage: function(u, st) { - if (!u) { - tinyMCEPopup.dom.setHTML('prev', ''); - return; - } - - if (!st && tinyMCEPopup.getParam("advimage_update_dimensions_onchange", true)) - this.resetImageData(); - - u = tinyMCEPopup.editor.documentBaseURI.toAbsolute(u); - - if (!st) - tinyMCEPopup.dom.setHTML('prev', ''); - else - tinyMCEPopup.dom.setHTML('prev', ''); - } -}; - -ImageDialog.preInit(); -tinyMCEPopup.onInit.add(ImageDialog.init, ImageDialog); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/en_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/en_dlg.js deleted file mode 100644 index 36c09935a4..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/en_dlg.js +++ /dev/null @@ -1,43 +0,0 @@ -tinyMCE.addI18n('en.umbimage_dlg', { - tab_general: 'General', - tab_appearance: 'Appearance', - tab_advanced: 'Advanced', - general: 'General', - title: 'Title', - preview: 'Preview', - constrain_proportions: 'Constrain proportions', - langdir: 'Language direction', - langcode: 'Language code', - long_desc: 'Long description link', - style: 'Style', - classes: 'Classes', - ltr: 'Left to right', - rtl: 'Right to left', - id: 'Id', - map: 'Image map', - swap_image: 'Swap image', - alt_image: 'Alternative image', - mouseover: 'for mouse over', - mouseout: 'for mouse out', - misc: 'Miscellaneous', - example_img: 'Appearance preview image', - missing_alt: 'Are you sure you want to continue without including an Image Description? Without it the image may not be accessible to some users with disabilities, or to those using a text browser, or browsing the Web with images turned off.', - dialog_title: 'Insert/edit image', - src: 'Image URL', - alt: 'Image description', - list: 'Image list', - border: 'Border', - dimensions: 'Dimensions', - vspace: 'Vertical space', - hspace: 'Horizontal space', - align: 'Alignment', - align_baseline: 'Baseline', - align_top: 'Top', - align_middle: 'Middle', - align_bottom: 'Bottom', - align_texttop: 'Text top', - align_textbottom: 'Text bottom', - align_left: 'Left', - align_right: 'Right', - image_list: 'Image list' -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/en_us_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/en_us_dlg.js deleted file mode 100644 index db5be8ae0b..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/en_us_dlg.js +++ /dev/null @@ -1,43 +0,0 @@ -tinyMCE.addI18n('en_us.umbimage_dlg', { - tab_general: 'General', - tab_appearance: 'Appearance', - tab_advanced: 'Advanced', - general: 'General', - title: 'Title', - preview: 'Preview', - constrain_proportions: 'Constrain proportions', - langdir: 'Language direction', - langcode: 'Language code', - long_desc: 'Long description link', - style: 'Style', - classes: 'Classes', - ltr: 'Left to right', - rtl: 'Right to left', - id: 'Id', - map: 'Image map', - swap_image: 'Swap image', - alt_image: 'Alternative image', - mouseover: 'for mouse over', - mouseout: 'for mouse out', - misc: 'Miscellaneous', - example_img: 'Appearance preview image', - missing_alt: 'Are you sure you want to continue without including an Image Description? Without it the image may not be accessible to some users with disabilities, or to those using a text browser, or browsing the Web with images turned off.', - dialog_title: 'Insert/edit image', - src: 'Image URL', - alt: 'Image description', - list: 'Image list', - border: 'Border', - dimensions: 'Dimensions', - vspace: 'Vertical space', - hspace: 'Horizontal space', - align: 'Alignment', - align_baseline: 'Baseline', - align_top: 'Top', - align_middle: 'Middle', - align_bottom: 'Bottom', - align_texttop: 'Text top', - align_textbottom: 'Text bottom', - align_left: 'Left', - align_right: 'Right', - image_list: 'Image list' -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/he_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/he_dlg.js deleted file mode 100644 index 98091a1b41..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/he_dlg.js +++ /dev/null @@ -1,43 +0,0 @@ -tinyMCE.addI18n('he.umbimage_dlg', { - tab_general: 'General', - tab_appearance: 'Appearance', - tab_advanced: 'Advanced', - general: 'General', - title: 'Title', - preview: 'Preview', - constrain_proportions: 'Constrain proportions', - langdir: 'Language direction', - langcode: 'Language code', - long_desc: 'Long description link', - style: 'Style', - classes: 'Classes', - ltr: 'Left to right', - rtl: 'Right to left', - id: 'Id', - map: 'Image map', - swap_image: 'Swap image', - alt_image: 'Alternative image', - mouseover: 'for mouse over', - mouseout: 'for mouse out', - misc: 'Miscellaneous', - example_img: 'Appearance preview image', - missing_alt: 'Are you sure you want to continue without including an Image Description? Without it the image may not be accessible to some users with disabilities, or to those using a text browser, or browsing the Web with images turned off.', - dialog_title: 'Insert/edit image', - src: 'Image URL', - alt: 'Image description', - list: 'Image list', - border: 'Border', - dimensions: 'Dimensions', - vspace: 'Vertical space', - hspace: 'Horizontal space', - align: 'Alignment', - align_baseline: 'Baseline', - align_top: 'Top', - align_middle: 'Middle', - align_bottom: 'Bottom', - align_texttop: 'Text top', - align_textbottom: 'Text bottom', - align_left: 'Left', - align_right: 'Right', - image_list: 'Image list' -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/it_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/it_dlg.js deleted file mode 100644 index d1b32b26c2..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/it_dlg.js +++ /dev/null @@ -1,43 +0,0 @@ -tinyMCE.addI18n('it.umbimage_dlg', { - tab_general: 'Generale', - tab_appearance: 'Aspetto', - tab_advanced: 'Avanzate', - general: 'Generale', - title: 'Titolo', - preview: 'Anteprima', - constrain_proportions: 'Vincola proporzioni', - langdir: 'Direzione lingua', - langcode: 'Codice lingua', - long_desc: 'Descrizione lunga del collegamento', - style: 'Stile', - classes: 'Classi', - ltr: 'Da sinistra a destra', - rtl: 'Da destra a sinistra', - id: 'Id', - map: 'Image map', - swap_image: 'Swap immagine', - alt_image: 'Testo alternativo', - mouseover: 'Mouse over', - mouseout: 'Mouse out', - misc: 'Varie', - example_img: 'Aspetto anteprima immagine', - missing_alt: 'Sei sicuro di voler continuare senza includere una Descrizione dell'immagine? Se non lo fai l'immagine potrebbe risultare non accessibile per gli utenti con disabilit\u00E0, o per chi utilizza un browser di testo, o per chi naviga senza immagini.', - dialog_title: 'Inserisci/Modifica immagine', - src: 'URL immagine', - alt: 'Descrizione immagine', - list: 'Immagine lista', - border: 'Bordo', - dimensions: 'Dimensioni', - vspace: 'Spaziatura verticale', - hspace: 'Spaziatura orizzontale', - align: 'Allineamento', - align_baseline: 'Baseline', - align_top: 'Top', - align_middle: 'Middle', - align_bottom: 'Bottom', - align_texttop: 'Testo superiore', - align_textbottom: 'Testo inferiore', - align_left: 'Sinistra', - align_right: 'Destra', - image_list: 'Immagine lista' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/ja_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/ja_dlg.js deleted file mode 100644 index 1140ea22c9..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/ja_dlg.js +++ /dev/null @@ -1,43 +0,0 @@ -tinyMCE.addI18n('ja.umbimage_dlg', { - tab_general: '一般', - tab_appearance: '表示', - tab_advanced: '高度な設定', - general: '一般', - title: 'タイトル', - preview: 'プレビュー', - constrain_proportions: '縦横比の維持', - langdir: '文章の方向', - langcode: '言語コード', - long_desc: '詳細な説明のリンク', - style: 'スタイル', - classes: 'クラス', - ltr: '左から右', - rtl: '右から左', - id: 'Id', - map: 'イメージマップ', - swap_image: '画像の入れ替え', - alt_image: '別の画像', - mouseover: 'マウスカーソルがかかる時', - mouseout: 'マウスカーソルが外れる時', - misc: 'その他', - example_img: '画像のプレビューの様子', - missing_alt: '画像の説明を含めずに続けますか?画像の説明がないと目の不自由な方、テキスト表示だけのブラウザを使用している方、画像の表示を止めてる方がアクセスできないかもしれません。', - dialog_title: '画像の挿入/編集', - src: '画像のURL', - alt: '画像の説明', - list: '画像の一覧', - border: '枠線', - dimensions: '寸法', - vspace: '上下の余白', - hspace: '左右の余白', - align: '配置', - align_baseline: 'ベースライン揃え', - align_top: '上揃え', - align_middle: '中央揃え', - align_bottom: '下揃え', - align_texttop: 'テキストの上端揃え', - align_textbottom: 'テキストの下端揃え', - align_left: '左寄せ', - align_right: '右寄せ', - image_list: '画像の一覧' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/ru_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/ru_dlg.js deleted file mode 100644 index 4cb8e5558a..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/ru_dlg.js +++ /dev/null @@ -1,43 +0,0 @@ -tinyMCE.addI18n('ru.umbimage_dlg', { - tab_general: 'Общее', - tab_appearance: 'Вид', - tab_advanced: 'Дополнительно', - general: 'Общие свойства', - title: 'Заголовок', - preview: 'Предпросмотр', - constrain_proportions: 'Сохранять пропорции', - langdir: 'Направление языка', - langcode: 'Код языка', - long_desc: 'Ссылка на длинное описание', - style: 'Стиль', - classes: 'Классы CSS', - ltr: 'Слева напрапво', - rtl: 'Справа налево', - id: 'Id', - map: 'Карта', - swap_image: 'Замена', - alt_image: 'Альтернатива', - mouseover: 'при заходе мыши', - mouseout: 'при выходе мыши', - misc: 'Разное', - example_img: 'Пример внешнего вида', - missing_alt: 'Вы уверены, что хотите продолжить без указания описания изображения? Без описания изображение может оказаться недоступным некоторым категориям пользователей с ограниченными возможностями, или использующим текстовый браузер, а также пользователям, отключившим показ изображений.', - dialog_title: 'Вставить/изменить изображение', - src: 'URL изображения', - alt: 'Описание изображения', - list: 'Список', - border: 'Рамка', - dimensions: 'Размеры', - vspace: 'Отступ по вертикали', - hspace: 'Отступ по горизонтали', - align: 'Выравнивание', - align_baseline: 'По базовой линии', - align_top: 'По верху', - align_middle: 'По центру', - align_bottom: 'По низу', - align_texttop: 'По верху текста', - align_textbottom: 'По низу текста', - align_left: 'По левому краю', - align_right: 'По правому краю', - image_list: 'Список изображений' -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/sv_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/sv_dlg.js deleted file mode 100644 index 2c18b280c5..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/sv_dlg.js +++ /dev/null @@ -1,43 +0,0 @@ -tinyMCE.addI18n('sv.umbimage_dlg', { - tab_general: 'Generellt', - tab_appearance: 'Utseende', - tab_advanced: 'Avancerat', - general: 'Generellt', - title: 'Titel', - preview: 'Förhandsgranska', - constrain_proportions: 'Bibehåll proportioner', - langdir: 'Språkdirektion', - langcode: 'Språkkod', - long_desc: 'Lång länkbeskrivning', - style: 'Stil', - classes: 'Klasser', - ltr: 'Vänster till höger', - rtl: 'höger till vänster', - id: 'Id', - map: 'Bildkarta', - swap_image: 'Byt bild', - alt_image: 'Alternativ bild', - mouseover: 'För musen över', - mouseout: 'för musen utanför', - misc: 'Blandat', - example_img: 'Visning av bildförhandsgranskning', - missing_alt: 'Are you sure you want to continue without including an Image Description? Without it the image may not be accessible to some users with disabilities, or to those using a text browser, or browsing the Web with images turned off.', - dialog_title: 'Infoga/redigera bild', - src: 'Bild URL', - alt: 'Bildbeskrivning', - list: 'Bildlista', - border: 'Ram', - dimensions: 'Dimensioner', - vspace: 'Vertikalt avstånd', - hspace: 'Horisontellt avstånd', - align: 'Position', - align_baseline: 'Baslinje', - align_top: 'Toppen', - align_middle: 'Mitten', - align_bottom: 'Botten', - align_texttop: 'Text topp', - align_textbottom: 'Text botten', - align_left: 'Vänster', - align_right: 'Höger', - image_list: 'Bildlista' -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/zh_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/zh_dlg.js deleted file mode 100644 index 449c6df44d..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoimg/langs/zh_dlg.js +++ /dev/null @@ -1,43 +0,0 @@ -tinyMCE.addI18n('zh.umbimage_dlg', { - tab_general: '普通', - tab_appearance: '外观', - tab_advanced: '高级', - general: '普通', - title: '标题', - preview: '预览', - constrain_proportions: '约束比例', - langdir: '语言书写方向', - langcode: '语言代码', - long_desc: '长原文链接', - style: '样式', - classes: '类', - ltr: '从左到右', - rtl: '从右到左', - id: 'Id', - map: '图片热区', - swap_image: '交换图片', - alt_image: '替代图片', - mouseover: '鼠标移入', - mouseout: '鼠标移出', - misc: '其它', - example_img: '样图外观', - missing_alt: '你确定不要图片替代文字吗?替代文字可以在图片无法显示时显示。', - dialog_title: '插入/编辑图片', - src: '图片URL', - alt: '图片描述', - list: '图片列表', - border: '边框', - dimensions: '尺寸', - vspace: '垂直间距', - hspace: '水平间距', - align: '对齐', - align_baseline: '对齐底线', - align_top: '顶部对齐', - align_middle: '中间对齐', - align_bottom: '底部对齐', - align_texttop: '对齐文字顶部', - align_textbottom: '对齐文字底部', - align_left: '左对齐', - align_right: '右对齐', - image_list: '图片列表' -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracolink/plugin.min.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracolink/plugin.min.js deleted file mode 100644 index b05334243a..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracolink/plugin.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * This file is intentionally left empty - * For reference, see: http://issues.umbraco.org/issue/U4-9724 - * The logic for the umbracoLink plugin now lives in ~/Umbraco/Js/umbraco.services.js - */ \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/dialog.htm b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/dialog.htm deleted file mode 100644 index b4c62840ea..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/dialog.htm +++ /dev/null @@ -1,27 +0,0 @@ - - - - {#example_dlg.title} - - - - - -
-

Here is a example dialog.

-

Selected text:

-

Custom arg:

- -
-
- -
- -
- -
-
-
- - - diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/editor_plugin_src.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/editor_plugin_src.js deleted file mode 100644 index 35fc20fc14..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/editor_plugin_src.js +++ /dev/null @@ -1,145 +0,0 @@ -/** -* $Id: editor_plugin_src.js 201 2007-02-12 15:56:56Z spocke $ -* -* @author Moxiecode -* @copyright Copyright 2004-2008, Moxiecode Systems AB, All rights reserved. -*/ - -(function() { - // Load plugin specific language pack -// tinymce.PluginManager.requireLangPack('umbraco'); - - tinymce.create('tinymce.plugins.umbracomacro', { - /** - * Initializes the plugin, this will be executed after the plugin has been created. - * This call is done before the editor instance has finished it's initialization so use the onInit event - * of the editor instance to intercept that event. - * - * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in. - * @param {string} url Absolute URL to where the plugin is located. - */ - init: function(ed, url) { - var t = this; - - // Register the command so that it can be invoked by using tinyMCE.activeEditor.execCommand('mceExample'); - ed.addCommand('mceumbracomacro', function() { - var se = ed.selection; - - var urlParams = ""; - var el = se.getNode(); - - // ie selector bug - if (!ed.dom.hasClass(el, 'umbMacroHolder')) { - el = ed.dom.getParent(el, 'div.umbMacroHolder'); - } - - var attrString = ""; - if (ed.dom.hasClass(el, 'umbMacroHolder')) { - for (var i = 0; i < el.attributes.length; i++) { - attrName = el.attributes[i].nodeName.toLowerCase(); - if (attrName != "mce_serialized") { - if (el.attributes[i].nodeValue && (attrName != 'ismacro' && attrName != 'style' && attrName != 'contenteditable')) { - attrString += el.attributes[i].nodeName + '=' + escape(t._utf8_encode(el.attributes[i].nodeValue)) + '&'; //.replace(/#/g, "%23").replace(/\/g, "%3E").replace(/\"/g, "%22") + '&'; - - } - } - } - - // vi trunkerer strengen ved at fjerne et evt. overskydende amp; - if (attrString.length > 0) - attrString = attrString.substr(0, attrString.length - 1); - - urlParams = "&" + attrString; - } else { - urlParams = '&umbPageId=' + tinyMCE.activeEditor.getParam('theme_umbraco_pageId') + '&umbVersionId=' + tinyMCE.activeEditor.getParam('theme_umbraco_versionId'); - } - - ed.windowManager.open({ - file: tinyMCE.activeEditor.getParam('umbraco_path') + '/plugins/tinymce3/insertMacro.aspx?editor=trueurl' + urlParams, - width: 480 + parseInt(ed.getLang('umbracomacro.delta_width', 0)), - height: 470 + parseInt(ed.getLang('umbracomacro.delta_height', 0)), - inline: 1 - }, { - plugin_url: url // Plugin absolute URL - }); - }); - - // Register example button - ed.addButton('umbracomacro', { - title: 'umbracomacro.desc', - cmd: 'mceumbracomacro', - image: url + '/img/insMacro.gif' - }); - - // Add a node change handler, test if we're editing a macro - ed.onNodeChange.addToTop(function(ed, cm, n) { - - var macroElement = ed.dom.getParent(ed.selection.getStart(), 'div.umbMacroHolder'); - - // mark button if it's a macro - cm.setActive('umbracomacro', macroElement && ed.dom.hasClass(macroElement, 'umbMacroHolder')); - - }); - }, - - _utf8_encode: function(string) { - string = string.replace(/\r\n/g, "\n"); - var utftext = ""; - - for (var n = 0; n < string.length; n++) { - - var c = string.charCodeAt(n); - - if (c < 128) { - utftext += String.fromCharCode(c); - } - else if ((c > 127) && (c < 2048)) { - utftext += String.fromCharCode((c >> 6) | 192); - utftext += String.fromCharCode((c & 63) | 128); - } - else { - utftext += String.fromCharCode((c >> 12) | 224); - utftext += String.fromCharCode(((c >> 6) & 63) | 128); - utftext += String.fromCharCode((c & 63) | 128); - } - - } - - return utftext; - }, - - /** - * Creates control instances based in the incomming name. This method is normally not - * needed since the addButton method of the tinymce.Editor class is a more easy way of adding buttons - * but you sometimes need to create more complex controls like listboxes, split buttons etc then this - * method can be used to create those. - * - * @param {String} n Name of the control to create. - * @param {tinymce.ControlManager} cm Control manager to use inorder to create new control. - * @return {tinymce.ui.Control} New control instance or null if no control was created. - */ - createControl: function(n, cm) { - return null; - }, - - - /** - * Returns information about the plugin as a name/value array. - * The current keys are longname, author, authorurl, infourl and version. - * - * @return {Object} Name/value array containing information about the plugin. - */ - getInfo: function() { - return { - longname: 'Umbraco Macro Insertion Plugin', - author: 'Umbraco', - authorurl: 'http://umbraco.org', - infourl: 'http://umbraco.org/redir/tinymcePlugins', - version: "1.0" - }; - } - }); - - // Register plugin - tinymce.PluginManager.add('umbracomacro', tinymce.plugins.umbracomacro); -})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/img/insMacro.gif b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/img/insMacro.gif deleted file mode 100644 index 43c58f4f03cc8702deda7ba83d6756d7a15211e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 603 zcmZ?wbhEHb6k!lyc*el+{Q1kbpZbFz3zh8d% zbLG|(Z$AAwaP{lrO($lp*t>k&nYpX?FIabI;krXhHyvHE^~B{zKiBR&y>aikL#J;( zdHehA=Rcpn|26eYp1f%L^kuuJE!{PB$n^m$|Gmo3p>wk3YqnelaR!MB6u-wsuOJ5u-USj+blZQoCJeLvOn{dE8Lvy*?E zpZ??GoS#>h{Jg&M=dJa>?rs0|V9)Qzhkidj_WSAafB*jd|Noz1pn>8~7Dfh!5C$EP zLqKujz`mm)q^Y^3wXKOSxU0KMjY(QWwoOiU>a^+8+BDqfxVbnv*xT9KSX)gF*40yI z=3z6}F%eUl)V^(d$ZU64XU9cLtqy9mDF>_TX_*BZXc{rF2yShW>tbePlw#rJ&7uhu%5f1&s_0)&Mvp3Vr|p diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/js/dialog.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/js/dialog.js deleted file mode 100644 index fa8341132f..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/js/dialog.js +++ /dev/null @@ -1,19 +0,0 @@ -tinyMCEPopup.requireLangPack(); - -var ExampleDialog = { - init : function() { - var f = document.forms[0]; - - // Get the selected contents as text and place it in the input - f.someval.value = tinyMCEPopup.editor.selection.getContent({format : 'text'}); - f.somearg.value = tinyMCEPopup.getWindowArg('some_custom_arg'); - }, - - insert : function() { - // Insert the contents from the input into the document - tinyMCEPopup.editor.execCommand('mceInsertContent', false, document.forms[0].someval.value); - tinyMCEPopup.close(); - } -}; - -tinyMCEPopup.onInit.add(ExampleDialog.init, ExampleDialog); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en.js deleted file mode 100644 index 60b03b55c7..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('en.umbracomacro',{ - desc : 'Insert macro' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_dlg.js deleted file mode 100644 index ebcf948dac..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_dlg.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('en.example_dlg',{ - title : 'This is just a example title' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_us.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_us.js deleted file mode 100644 index 61fee28c63..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_us.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('en_us.umbracomacro',{ - desc : 'Insert macro' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_us_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_us_dlg.js deleted file mode 100644 index 0468c4553c..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/en_us_dlg.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('en_us.example_dlg',{ - title : 'This is just a example title' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/he.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/he.js deleted file mode 100644 index 09319cceaa..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/he.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('he.umbracomacro',{ - desc : 'הוסף מאקרו' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/he_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/he_dlg.js deleted file mode 100644 index 390eabc168..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/he_dlg.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('he.example_dlg',{ - title : 'This is just a example title' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ja.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ja.js deleted file mode 100644 index 32e79f18c6..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ja.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('ja.umbracomacro',{ - desc : 'マクロの挿入' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ja_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ja_dlg.js deleted file mode 100644 index 67f4140f92..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ja_dlg.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('ja.example_dlg',{ - title : 'これはタイトルの例です' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ru.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ru.js deleted file mode 100644 index f9a98c4fb0..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ru.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('ru.umbracomacro',{ - desc : 'Вставить макрос' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ru_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ru_dlg.js deleted file mode 100644 index 3fa610a3ee..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/ru_dlg.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('ru.example_dlg',{ - title : ' ' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/sv.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/sv.js deleted file mode 100644 index fc134d5698..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/sv.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('sv.umbracomacro',{ - desc : 'Infoga makro' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/sv_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/sv_dlg.js deleted file mode 100644 index 3bf4ed0880..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/sv_dlg.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('sv.example_dlg',{ - title : 'Detta är bar ett exempel på en titel' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/zh.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/zh.js deleted file mode 100644 index f2edf9598f..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/zh.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('zh.umbracomacro',{ - desc : '插入宏' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/zh_dlg.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/zh_dlg.js deleted file mode 100644 index db7ad925a0..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracomacro/langs/zh_dlg.js +++ /dev/null @@ -1,3 +0,0 @@ -tinyMCE.addI18n('zh.example_dlg',{ - title : '这是示例标题' -}); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracopaste/editor_plugin_src.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracopaste/editor_plugin_src.js deleted file mode 100644 index aaf58e7c2d..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracopaste/editor_plugin_src.js +++ /dev/null @@ -1,53 +0,0 @@ -/** -* editor_plugin_src.js -* -* Copyright 2012, Umbraco -* Released under MIT License. -* -* License: http://opensource.org/licenses/mit-license.html -*/ - -(function () { - var Event = tinymce.dom.Event, each = tinymce.each, DOM = tinymce.DOM; - - /** - * This plugin modifies the standard TinyMCE paste, with umbraco specific changes. - * - * @class tinymce.plugins.umbContextMenu - */ - tinymce.create('tinymce.plugins.UmbracoPaste', { - /** - * Initializes the plugin, this will be executed after the plugin has been created. - * This call is done before the editor instance has finished it's initialization so use the onInit event - * of the editor instance to intercept that event. - * - * @method init - * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in. - * @param {string} url Absolute URL to where the plugin is located. - */ - init: function (ed) { - var t = this; - - ed.plugins.paste.onPreProcess.add(function (pl, o) { - - var ed = this.editor, h = o.content; - - var umbracoAllowedStyles = ed.getParam('theme_umbraco_styles'); - for (var i = 1; i < 7; i++) { - if (umbracoAllowedStyles.indexOf("h" + i) == -1) { - h = h.replace(new RegExp(']*', 'gi'), '

', 'gi'), '

'); - } - } - - o.content = h; - - }); - - } - - }); - - // Register plugin - tinymce.PluginManager.add('umbracopaste', tinymce.plugins.UmbracoPaste); -})(); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoshortcut/editor_plugin_src.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoshortcut/editor_plugin_src.js deleted file mode 100644 index 15d669b4e1..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracoshortcut/editor_plugin_src.js +++ /dev/null @@ -1,43 +0,0 @@ - -(function () { - tinymce.create('tinymce.plugins.Umbracoshortcut', { - init: function (ed, url) { - var t = this; - var ctrlPressed = false; - - t.editor = ed; - - ed.onKeyDown.add(function (ed, e) { - if (e.keyCode == 17) - ctrlPressed = true; - - if (ctrlPressed && e.keyCode == 83) { - jQuery(document).trigger("UMBRACO_TINYMCE_SAVE", e); - ctrlPressed = false; - tinymce.dom.Event.cancel(e); - return false; - } - }); - - ed.onKeyUp.add(function (ed, e) { - if (e.keyCode == 17) - ctrlPressed = false; - }); - }, - - getInfo: function () { - return { - longname: 'Umbraco Save short cut key', - author: 'Umbraco HQ', - authorurl: 'http://umbraco.com', - infourl: 'http://our.umbraco.org', - version: "1.0" - }; - } - - // Private methods - }); - - // Register plugin - tinymce.PluginManager.add('umbracoshortcut', tinymce.plugins.Umbracoshortcut); -})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index 60bb4957ae..e3a50b482c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -358,7 +358,7 @@ angular.module("umbraco.directives") var unsubscribe = scope.$on("formSubmitting", function () { //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer // we do parse it out on the server side but would be nice to do that on the client side before as well. - scope.value = tinyMceEditor.getContent(); + scope.value = tinyMceEditor ? tinyMceEditor.getContent() : null; }); //when the element is disposed we need to unsubscribe! diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 485b674594..b5beef2ff7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -365,7 +365,7 @@ angular.module("umbraco") var unsubscribe = $scope.$on("formSubmitting", function () { //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer // we do parse it out on the server side but would be nice to do that on the client side before as well. - $scope.model.value = tinyMceEditor.getContent(); + $scope.model.value = tinyMceEditor ? tinyMceEditor.getContent() : null; }); //when the element is disposed we need to unsubscribe! diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 8bf705f9b7..36d86209fe 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -124,9 +124,6 @@ ..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll True - - ..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll - ..\packages\dotless.1.5.2\lib\dotless.Core.dll @@ -367,20 +364,6 @@ True Settings.settings
- - ImageViewer.ascx - ASPXCodeBehind - - - ImageViewer.ascx - - - passwordChanger.ascx - ASPXCodeBehind - - - passwordChanger.ascx - create.aspx ASPXCodeBehind @@ -388,48 +371,13 @@ create.aspx - - DlrScripting.ascx - ASPXCodeBehind - - - DlrScripting.ascx - - - PartialView.ascx - ASPXCodeBehind - - - PartialView.ascx - - - PartialViewMacro.ascx - ASPXCodeBehind - - - PartialViewMacro.ascx - xslt.ascx ASPXCodeBehind - - User.ascx - ASPXCodeBehind - xslt.ascx - - User.ascx - - - ExamineManagement.ascx - ASPXCodeBehind - - - ExamineManagement.ascx - UserControlProxy.aspx ASPXCodeBehind @@ -469,13 +417,6 @@ EditMacro.aspx ASPXCodeBehind - - moveOrCopy.aspx - ASPXCodeBehind - - - moveOrCopy.aspx - sort.aspx ASPXCodeBehind @@ -589,8 +530,6 @@ - - @@ -676,8 +615,6 @@ - - @@ -790,17 +727,10 @@ - - - - - - - @@ -821,8 +751,6 @@ - - @@ -831,8 +759,6 @@ - - @@ -847,12 +773,9 @@ - - - @@ -862,8 +785,6 @@ - - @@ -875,17 +796,9 @@ - - - - - - - - @@ -894,7 +807,6 @@ - @@ -905,9 +817,6 @@ - - - Designer @@ -1024,8 +933,6 @@ UserControl - - diff --git a/src/Umbraco.Web.UI/Umbraco/create/PartialView.ascx b/src/Umbraco.Web.UI/Umbraco/create/PartialView.ascx deleted file mode 100644 index 8e8e53e196..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/create/PartialView.ascx +++ /dev/null @@ -1,31 +0,0 @@ -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="PartialView.ascx.cs" Inherits="Umbraco.Web.UI.Umbraco.Create.PartialView" %> -<%@ Import Namespace="umbraco" %> -<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - * - - Cannot end with '/' or '.' - - - - - - - - - -
[DataContract(Name = "user", Namespace = "")] - public class UserInvite : EntityBasic + public class UserInvite : EntityBasic, IValidatableObject { [DataMember(Name = "userGroups")] [Required] @@ -21,6 +22,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "message")] public string Message { get; set; } - + + public IEnumerable Validate(ValidationContext validationContext) + { + if (UserGroups.Any() == false) + yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs index 30f6bfc420..1b6a76ae99 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing @@ -47,11 +48,8 @@ namespace Umbraco.Web.Models.ContentEditing public IEnumerable Validate(ValidationContext validationContext) { - //TODO: Add other server side validation - //if (CultureInfo.GetCultureInfo(Culture)) - // yield return new ValidationResult("The culture is invalid", new[] { "Culture" }); - - yield break; + if (UserGroups.Any() == false) + yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); } } } \ No newline at end of file From fc8c750fb04e546f593a2c9cac51e24181a7e710 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 8 Aug 2017 10:22:44 +0200 Subject: [PATCH 546/714] fixes permissions click area - only show pointer cursor where you can click --- .../src/less/components/users/umb-permission.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-permission.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-permission.less index c7a898164c..faed098fc8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-permission.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-permission.less @@ -2,7 +2,6 @@ display: flex; border-bottom: 1px solid @gray-9; padding: 7px 0; - cursor: pointer; } .umb-permission:last-of-type { @@ -11,12 +10,14 @@ .umb-permission__toggle { padding-right: 20px; + cursor: pointer; } .umb-permission__content { display: flex; flex-direction: column; flex: 1 1 auto; + cursor: pointer; } .umb-permission__description { From 55a4214a5c08e166f933ff9cc7ddd468aebfb96e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 8 Aug 2017 13:05:46 +0200 Subject: [PATCH 547/714] fixes: U4-10233 Umbraco 7.7 beta - format user last login date --- .../src/views/users/user.controller.js | 22 ++++++++++++++++++- .../src/views/users/user.html | 5 +++-- .../users/views/users/users.controller.js | 15 +++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 237c44255c..e060eaf393 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, $timeout, $location, $routeParams, formHelper, usersResource, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource) { + function UserEditController($scope, $timeout, $location, $routeParams, formHelper, usersResource, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) { var vm = this; @@ -66,6 +66,11 @@ makeBreadcrumbs(vm.user); setUserDisplayState(); + // format dates to local + if(vm.user.lastLoginDate) { + vm.user.formattedLastLogin = getLocalDate(vm.user.lastLoginDate, "MMMM Do YYYY, HH:mm"); + } + vm.emailIsUsername = user.email === user.username; //go get the config for the membership provider and add it to the model @@ -81,6 +86,21 @@ }); }); } + + function getLocalDate(date, format) { + var dateVal; + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + var localOffset = new Date().getTimezoneOffset(); + var serverTimeNeedsOffsetting = (-serverOffset !== localOffset); + + if(serverTimeNeedsOffsetting) { + dateVal = dateHelper.convertToLocalMomentTime(date, serverOffset); + } else { + dateVal = moment(date, "YYYY-MM-DD HH:mm:ss"); + } + + return dateVal.format(format); + } function toggleChangePassword() { vm.changePasswordModel.isChanging = !vm.changePasswordModel.isChanging; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 84e50a3360..94c8360346 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -225,9 +225,10 @@
- Last login:
+ Last login: +
- {{ vm.user.lastLoginDate }} + {{ vm.user.formattedLastLogin }} {{ vm.user.name }} has not logged in yet
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 96db5e07b1..69db1a1da1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UsersController($scope, $timeout, $location, usersResource, userGroupsResource, localizationService, contentEditingHelper, usersHelper, formHelper, notificationsService) { + function UsersController($scope, $timeout, $location, usersResource, userGroupsResource, localizationService, contentEditingHelper, usersHelper, formHelper, notificationsService, dateHelper) { var vm = this; var localizeSaving = localizationService.localize("general_saving"); @@ -539,7 +539,18 @@ function formatDates(users) { angular.forEach(users, function (user) { if (user.lastLoginDate) { - user.formattedLastLogin = moment(user.lastLoginDate).format("MMMM Do YYYY, HH:mm"); + var dateVal; + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + var localOffset = new Date().getTimezoneOffset(); + var serverTimeNeedsOffsetting = (-serverOffset !== localOffset); + + if(serverTimeNeedsOffsetting) { + dateVal = dateHelper.convertToLocalMomentTime(user.lastLoginDate, serverOffset); + } else { + dateVal = moment(user.lastLoginDate, "YYYY-MM-DD HH:mm:ss"); + } + + user.formattedLastLogin = dateVal.format("MMMM Do YYYY, HH:mm"); } }); } From 8b962cff0c197f1d8585287f435d2ba03bc48e4a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 8 Aug 2017 16:07:16 +0100 Subject: [PATCH 548/714] Catches a dictionary key not found exception & gives better error message with context --- src/Umbraco.Core/Models/ContentBase.cs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 0fc3bac044..81808473da 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -264,7 +264,17 @@ namespace Umbraco.Core.Models /// Value as an public virtual object GetValue(string propertyTypeAlias) { - return Properties[propertyTypeAlias].Value; + try + { + return Properties[propertyTypeAlias].Value; + } + catch (KeyNotFoundException ex) + { + var message = string.Format("Cannot find the property with alias '{0}' for the node named '{1}' with id '{2} that uses the document type alias '{3}'", + propertyTypeAlias, this.Name, this.Id, this.ContentTypeBase.Alias); + + throw new Exception(message, ex); + } } /// @@ -275,8 +285,18 @@ namespace Umbraco.Core.Models /// Value as a public virtual TPassType GetValue(string propertyTypeAlias) { - var convertAttempt = Properties[propertyTypeAlias].Value.TryConvertTo(); - return convertAttempt.Success ? convertAttempt.Result : default(TPassType); + try + { + var convertAttempt = Properties[propertyTypeAlias].Value.TryConvertTo(); + return convertAttempt.Success ? convertAttempt.Result : default(TPassType); + } + catch (KeyNotFoundException ex) + { + var message = string.Format("Cannot find the property with alias '{0}' for the node named '{1}' with id '{2} that uses the document type alias '{3}'", + propertyTypeAlias, this.Name, this.Id, this.ContentTypeBase.Alias); + + throw new Exception(message, ex); + } } /// From fb4af749160681c0b360efb411de35b19c30c0e7 Mon Sep 17 00:00:00 2001 From: Jeavon Date: Tue, 8 Aug 2017 16:50:17 +0100 Subject: [PATCH 549/714] Fix so that health check notifications without config don't cause all notification methods to fail --- .../HealthCheckNotificationMethodResolver.cs | 33 ++++++++++++++----- .../EmailNotificationMethod.cs | 6 ++-- .../IHealthCheckNotificatationMethod.cs | 1 + .../Scheduling/HealthCheckNotifier.cs | 4 +-- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckNotificationMethodResolver.cs b/src/Umbraco.Web/HealthCheck/HealthCheckNotificationMethodResolver.cs index 3e676a0621..e7c6326a89 100644 --- a/src/Umbraco.Web/HealthCheck/HealthCheckNotificationMethodResolver.cs +++ b/src/Umbraco.Web/HealthCheck/HealthCheckNotificationMethodResolver.cs @@ -59,20 +59,37 @@ namespace Umbraco.Web.HealthCheck var healthCheckConfig = UmbracoConfig.For.HealthCheck(); var notificationMethods = healthCheckConfig.NotificationSettings.NotificationMethods; var notificationMethod = notificationMethods[attribute.Alias]; - if (notificationMethod == null) - { - return null; - } + // Create array for constructor paramenters. Will consists of common ones that all notification methods have as well // as those specific to this particular notification method. var baseType = typeof(NotificationMethodBase); var baseTypeCtor = baseType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).First(); var baseTypeCtorParamNames = baseTypeCtor.GetParameters().Select(x => x.Name); - var ctorParams = new List { notificationMethod.Enabled, notificationMethod.FailureOnly, notificationMethod.Verbosity }; - ctorParams.AddRange(ctor.GetParameters() - .Where(x => baseTypeCtorParamNames.Contains(x.Name) == false) - .Select(x => notificationMethod.Settings[x.Name].Value)); + + List ctorParams; + + if (notificationMethod != null) + { + // configuration found so set ctorParams to config values + ctorParams = new List + { + notificationMethod.Enabled, + notificationMethod.FailureOnly, + notificationMethod.Verbosity + }; + ctorParams.AddRange(ctor.GetParameters() + .Where(x => baseTypeCtorParamNames.Contains(x.Name) == false) + .Select(x => notificationMethod.Settings[x.Name].Value)); + } + else + { + // no configuration found so set to default values, enabled = false + ctorParams = new List { false, false, HealthCheckNotificationVerbosity.Detailed }; + ctorParams.AddRange(ctor.GetParameters() + .Where(x => baseTypeCtorParamNames.Contains(x.Name) == false) + .Select(x => string.Empty)); + } // Instantiate the type with the constructor parameters return Activator.CreateInstance(serviceType, ctorParams.ToArray()); diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs index bfce87fde0..9703fb775b 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods public EmailNotificationMethod(bool enabled, bool failureOnly, HealthCheckNotificationVerbosity verbosity, string recipientEmail) : this(enabled, failureOnly, verbosity, recipientEmail, ApplicationContext.Current.Services.TextService) - { + { } /// @@ -39,13 +39,13 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods : base(enabled, failureOnly, verbosity) { if (textService == null) throw new ArgumentNullException("textService"); - if (string.IsNullOrWhiteSpace(recipientEmail)) throw new ArgumentException("Value cannot be null or whitespace.", "recipientEmail"); + if (enabled && string.IsNullOrWhiteSpace(recipientEmail)) throw new ArgumentException("Value cannot be null or whitespace.", "recipientEmail"); _textService = textService; RecipientEmail = recipientEmail; Verbosity = verbosity; } - public string RecipientEmail { get; private set; } + public string RecipientEmail { get; private set; } public async Task SendAsync(HealthCheckResults results) { diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/IHealthCheckNotificatationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/IHealthCheckNotificatationMethod.cs index 1b7952c111..3202adc8e0 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/IHealthCheckNotificatationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/IHealthCheckNotificatationMethod.cs @@ -4,6 +4,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods { public interface IHealthCheckNotificatationMethod { + bool Enabled { get; } Task SendAsync(HealthCheckResults results); } } diff --git a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs index 61296a37bd..c10c1c5315 100644 --- a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs +++ b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs @@ -64,8 +64,8 @@ namespace Umbraco.Web.Scheduling var results = new HealthCheckResults(checks); results.LogResults(); - // Send using registered notification methods - var registeredNotificationMethods = HealthCheckNotificationMethodResolver.Current.NotificationMethods; + // Send using registered notification methods that are enabled + var registeredNotificationMethods = HealthCheckNotificationMethodResolver.Current.NotificationMethods.Where(x => x.Enabled); foreach (var notificationMethod in registeredNotificationMethods) { await notificationMethod.SendAsync(results); From a851cef70f692a0a9026f0a12702885c1557492c Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 8 Aug 2017 18:54:15 +0200 Subject: [PATCH 550/714] Improve EnsureUniqueNodeName perfs? --- .../Repositories/ContentRepository.cs | 27 +-- .../DataTypeDefinitionRepository.cs | 29 +--- .../Repositories/MediaRepository.cs | 27 +-- .../Repositories/SimilarNodeName.cs | 112 +++++++++++++ .../Repositories/SimilarNodeNameComparer.cs | 45 ----- src/Umbraco.Core/Umbraco.Core.csproj | 2 +- .../Repositories/SimilarNodeNameTests.cs | 98 +++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + src/Umbraco.Web.UI/config/Dashboard.config | 10 ++ .../config/splashes/noNodes.aspx | 155 ++++++++++++++---- 10 files changed, 354 insertions(+), 152 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs delete mode 100644 src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs create mode 100644 src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index addd0bc925..e8df52e9d3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1126,31 +1126,10 @@ ORDER BY cmsContentVersion.id DESC if (EnsureUniqueNaming == false) return nodeName; - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName)); + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 8c263f4211..44be068b4b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -450,33 +450,10 @@ AND umbracoNode.id <> @id", private string EnsureUniqueNodeName(string nodeName, int id = 0) { + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType", + new { objectType = NodeObjectTypeId }); - - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Text.StartsWith(nodeName)); - - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index ee413c2a59..bcb1d006a8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -584,31 +584,10 @@ namespace Umbraco.Core.Persistence.Repositories if (EnsureUniqueNaming == false) return nodeName; - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName)); + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs new file mode 100644 index 0000000000..371f73b27f --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Persistence.Repositories +{ + internal class SimilarNodeName + { + private int _numPos = -2; + + public int Id { get; set; } + public string Name { get; set; } + + // cached - reused + public int NumPos + { + get + { + if (_numPos != -2) return _numPos; + + var name = Name; + + if (name[name.Length - 1] != ')') + return _numPos = -1; + + var pos = name.LastIndexOf('('); + if (pos < 2 || pos == name.Length - 2) // < 2 and not < 0, because we want at least "x (" + return _numPos = -1; + + return _numPos = pos; + } + } + + // not cached - used only once + public int NumVal + { + get + { + if (NumPos < 0) + throw new InvalidOperationException(); + int num; + if (int.TryParse(Name.Substring(NumPos + 1, Name.Length - 2 - NumPos), out num)) + return num; + return 0; + } + } + + // compare without allocating, nor parsing integers + internal class Comparer : IComparer + { + public int Compare(SimilarNodeName x, SimilarNodeName y) + { + if (x == null) throw new ArgumentNullException("x"); + if (y == null) throw new ArgumentNullException("y"); + + var xpos = x.NumPos; + var ypos = y.NumPos; + + var xname = x.Name; + var yname = y.Name; + + if (xpos < 0 || ypos < 0 || xpos != ypos) + return string.Compare(xname, yname, StringComparison.Ordinal); + + // compare the part before (number) + var n = string.Compare(xname, 0, yname, 0, xpos, StringComparison.Ordinal); + if (n != 0) + return n; + + // compare (number) lengths + var diff = xname.Length - yname.Length; + if (diff != 0) return diff < 0 ? -1 : +1; + + // actually compare (number) + var i = xpos; + while (i < xname.Length - 1) + { + if (xname[i] != yname[i]) + return xname[i] < yname[i] ? -1 : +1; + i++; + } + return 0; + } + } + + // gets a unique name + public static string GetUniqueName(IEnumerable names, int nodeId, string nodeName) + { + var uniqueNumber = 1; + var uniqueing = false; + foreach (var name in names.OrderBy(x => x, new Comparer())) + { + // ignore self + if (nodeId != 0 && name.Id == nodeId) continue; + + if (uniqueing) + { + if (name.NumPos > 0 && name.Name.StartsWith(nodeName) && name.NumVal == uniqueNumber) + uniqueNumber++; + else + break; + } + else if (name.Name.InvariantEquals(nodeName)) + { + uniqueing = true; + } + } + + return uniqueing ? string.Concat(nodeName, " (", uniqueNumber.ToString(), ")") : nodeName; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs deleted file mode 100644 index ce25486422..0000000000 --- a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Core.Persistence.Repositories -{ - /// - /// Comparer that takes into account the duplicate index of a node name - /// This is needed as a normal alphabetic sort would go Page (1), Page (10), Page (2) etc. - /// - internal class SimilarNodeNameComparer : IComparer - { - public int Compare(string x, string y) - { - if (x.LastIndexOf('(') != -1 && x.LastIndexOf(')') == x.Length - 1 && y.LastIndexOf(')') == y.Length - 1) - { - if (x.ToLower().Substring(0, x.LastIndexOf('(')) == y.ToLower().Substring(0, y.LastIndexOf('('))) - { - int xDuplicateIndex = ExtractDuplicateIndex(x); - int yDuplicateIndex = ExtractDuplicateIndex(y); - - if (xDuplicateIndex != 0 && yDuplicateIndex != 0) - { - return xDuplicateIndex.CompareTo(yDuplicateIndex); - } - } - } - return String.Compare(x.ToLower(), y.ToLower(), StringComparison.Ordinal); - } - - private int ExtractDuplicateIndex(string text) - { - int index = 0; - - if (text.LastIndexOf('(') != -1 && text.LastIndexOf('(') < text.Length - 2) - { - int startPos = text.LastIndexOf('(') + 1; - int length = text.Length - 1 - startPos; - - int.TryParse(text.Substring(startPos, length), out index); - } - - return index; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 96183f61bf..8931fa1b23 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -628,6 +628,7 @@ + @@ -1230,7 +1231,6 @@ - diff --git a/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs new file mode 100644 index 0000000000..72f3e39874 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core.Persistence.Repositories; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + public class SimilarNodeNameTests + { + [TestCase("Alpha", "Alpha", 0)] + [TestCase("Alpha", "ALPHA", +1)] // case is important + [TestCase("Alpha", "Bravo", -1)] + [TestCase("Bravo", "Alpha", +1)] + [TestCase("Alpha (1)", "Alpha (1)", 0)] + [TestCase("Alpha", "Alpha (1)", -1)] + [TestCase("Alpha (1)", "Alpha", +1)] + [TestCase("Alpha (1)", "Alpha (2)", -1)] + [TestCase("Alpha (2)", "Alpha (1)", +1)] + [TestCase("Alpha (2)", "Alpha (10)", -1)] // this is the real stuff + [TestCase("Alpha (10)", "Alpha (2)", +1)] // this is the real stuff + [TestCase("Kilo", "Golf (2)", +1)] + [TestCase("Kilo (1)", "Golf (2)", +1)] + public void ComparerTest(string name1, string name2, int expected) + { + var comparer = new SimilarNodeName.Comparer(); + + var result = comparer.Compare(new SimilarNodeName { Name = name1 }, new SimilarNodeName { Name = name2 }); + if (expected == 0) + Assert.AreEqual(0, result); + else if (expected < 0) + Assert.IsTrue(result < 0, "Expected <0 but was " + result); + else if (expected > 0) + Assert.IsTrue(result > 0, "Expected >0 but was " + result); + } + + [Test] + public void OrderByTest() + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Alpha" }, + new SimilarNodeName { Id = 3, Name = "Golf" }, + new SimilarNodeName { Id = 4, Name = "Zulu" }, + new SimilarNodeName { Id = 5, Name = "Mike" }, + new SimilarNodeName { Id = 6, Name = "Kilo (1)" }, + new SimilarNodeName { Id = 7, Name = "Yankee" }, + new SimilarNodeName { Id = 8, Name = "Kilo" }, + new SimilarNodeName { Id = 9, Name = "Golf (2)" }, + new SimilarNodeName { Id = 10, Name = "Alpha (1)" }, + }; + + var ordered = names.OrderBy(x => x, new SimilarNodeName.Comparer()).ToArray(); + + var i = 0; + Assert.AreEqual(2, ordered[i++].Id); + Assert.AreEqual(10, ordered[i++].Id); + Assert.AreEqual(1, ordered[i++].Id); + Assert.AreEqual(3, ordered[i++].Id); + Assert.AreEqual(9, ordered[i++].Id); + Assert.AreEqual(8, ordered[i++].Id); + Assert.AreEqual(6, ordered[i++].Id); + Assert.AreEqual(5, ordered[i++].Id); + Assert.AreEqual(7, ordered[i++].Id); + Assert.AreEqual(4, ordered[i++].Id); + } + + [TestCase(0, "Charlie", "Charlie")] + [TestCase(0, "Zulu", "Zulu (1)")] + [TestCase(0, "Golf", "Golf (1)")] + [TestCase(0, "Kilo", "Kilo (2)")] + [TestCase(0, "Alpha", "Alpha (3)")] + [TestCase(0, "Kilo (1)", "Kilo (1) (1)")] // though... we might consider "Kilo (2)" + [TestCase(6, "Kilo (1)", "Kilo (1)")] // because of the id + public void Test(int nodeId, string nodeName, string expected) + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Alpha" }, + new SimilarNodeName { Id = 3, Name = "Golf" }, + new SimilarNodeName { Id = 4, Name = "Zulu" }, + new SimilarNodeName { Id = 5, Name = "Mike" }, + new SimilarNodeName { Id = 6, Name = "Kilo (1)" }, + new SimilarNodeName { Id = 7, Name = "Yankee" }, + new SimilarNodeName { Id = 8, Name = "Kilo" }, + new SimilarNodeName { Id = 9, Name = "Golf (2)" }, + new SimilarNodeName { Id = 10, Name = "Alpha (1)" }, + }; + + Assert.AreEqual(expected, SimilarNodeName.GetUniqueName(names, nodeId, nodeName)); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 92d00e08dc..0824e837e1 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -188,6 +188,7 @@ + diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config index 322551545d..c8c7e32f27 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.config +++ b/src/Umbraco.Web.UI/config/Dashboard.config @@ -119,4 +119,14 @@ +
+ + content + + + + /App_Plugins/Deploy/views/dashboards/dashboard.html + + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx index d46ecd6113..c1ceabf05b 100644 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx @@ -1,7 +1,7 @@ -<%@ Page Language="C#" AutoEventWireup="True" Inherits="Umbraco.Web.UI.Config.Splashes.NoNodes" CodeBehind="NoNodes.aspx.cs" %> -<%@ Import Namespace="Umbraco.Core.Configuration" %> +<%@ Page Language="C#" AutoEventWireup="true"%> <%@ Import Namespace="Umbraco.Core.IO" %> - +<%@ Import Namespace="Umbraco.Deploy.UI" %> +<%@ Import Namespace="Umbraco.Web" %> @@ -11,51 +11,142 @@ - + - + + + + + + + +<% if(HttpContext.Current.Request.IsLocal == false){ %>
-
-
- +
+
+ -

Welcome to your Umbraco installation

-

You're seeing this wonderful page because your website doesn't contain any published content yet.

+

Welcome to your Umbraco installation

+

You're seeing the wonderful page because your website doesn't contain any published content yet.

- + -
-
-

Easy start with Umbraco.tv

-

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

- - Umbraco.tv → -
+
+
+

Easy start with Umbraco.tv

+

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

+ + Umbraco.tv → +
-
-

Be a part of the community

-

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

- - our.Umbraco → -
-
+
+

Be a part of the community

+

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we’re sure that you can get your answers from the community.

+ + our.Umbraco → +
+
-
-
+
+
+
+ +<% }else{ %> + +
+
+ +
+ + +
+ +
+ +

Initializing...

+ Please wait while your site is initialized + +

{{ vm.restore.restoreProgress }}% restored

+ {{ vm.restore.currentActivity }} + +

Initializing your site...

+ Press the button below to begin +
+ +
+ +
+ +
+

Restore from Umbraco Cloud

+ +
+ +
+

Restoring your website...

+

{{ vm.restore.restoreProgress }}% restored

+ {{ vm.restore.currentActivity }} +
+ +
+

Ready to rock n' roll!

+

Everything has been restored and is ready for use, click below to open Umbraco

+ +
+ + + + + <%--
+

An error occurred:

+

{{ vm.restore.error.exceptionMessage }}

+ Show details +
{{ vm.restore.error.log }}
+
--%> + +
+ +
+
- - +<%= NoNodesHelper.ServerVariables(HttpContext.Current.Request.RequestContext, UmbracoContext.Current) %> + + + + + + + + + +<% } %> - + \ No newline at end of file From 2c2a9cc9161474a3ecc083d88a12cb5efa7566e7 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 9 Aug 2017 10:47:41 +0200 Subject: [PATCH 551/714] fixes: U4-10220 Missing the ability to create Content Templates (blueprints) from the blueprints tree directly when none exist --- .../contentblueprints/create.controller.js | 35 ++++++++++++++++ .../src/views/contentblueprints/create.html | 41 +++++++++++++++++++ .../umbraco/config/lang/en_us.xml | 1 + .../Trees/ContentBlueprintTreeController.cs | 5 ++- 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.controller.js new file mode 100644 index 0000000000..31089f4281 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.controller.js @@ -0,0 +1,35 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.ContentBlueprint.CreateController + * @function + * + * @description + * The controller for creating content blueprints + */ +function ContentBlueprintCreateController($scope, $location, contentTypeResource, navigationService) { + + var vm = this; + var node = $scope.dialogOptions.currentNode; + + vm.createBlueprint = createBlueprint; + + function onInit() { + + vm.loading = true; + + contentTypeResource.getAll() + .then(function (documentTypes) { + vm.documentTypes = documentTypes; + vm.loading = false; + }); + } + + function createBlueprint(documentType) { + $location.path("/settings/contentBlueprints/edit/" + node.id).search("create", "true").search("doctype", documentType.alias); + navigationService.hideMenu(); + } + + onInit(); +} + +angular.module("umbraco").controller("Umbraco.Editors.ContentBlueprint.CreateController", ContentBlueprintCreateController); diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html new file mode 100644 index 0000000000..7d04dfa771 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html @@ -0,0 +1,41 @@ +
+ + + + + +
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 26d6e587b5..8041c9c08a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -224,6 +224,7 @@ Where do you want to create the new %0% Create an item under + Select the document type you want to make a content template for Choose a type and a title "document types".]]> "media types".]]> diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index 12fcff2f56..0f9da51e63 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -100,8 +100,9 @@ namespace Umbraco.Web.Trees var menu = new MenuItemCollection(); if (id == Constants.System.Root.ToInvariantString()) - { - // root actions + { + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); return menu; } From b60c700e054cf8cd4fbac46de56ba5b50a42efc3 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 9 Aug 2017 10:23:55 +0100 Subject: [PATCH 552/714] Updated to return nulls as opposed to the ex being caught with a meaningful message --- src/Umbraco.Core/Models/ContentBase.cs | 27 ++++++-------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 81808473da..32eec7f284 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -264,17 +264,7 @@ namespace Umbraco.Core.Models /// Value as an public virtual object GetValue(string propertyTypeAlias) { - try - { - return Properties[propertyTypeAlias].Value; - } - catch (KeyNotFoundException ex) - { - var message = string.Format("Cannot find the property with alias '{0}' for the node named '{1}' with id '{2} that uses the document type alias '{3}'", - propertyTypeAlias, this.Name, this.Id, this.ContentTypeBase.Alias); - - throw new Exception(message, ex); - } + return Properties.Contains(propertyTypeAlias) ? Properties[propertyTypeAlias].Value : null; } /// @@ -285,18 +275,13 @@ namespace Umbraco.Core.Models /// Value as a public virtual TPassType GetValue(string propertyTypeAlias) { - try + if (Properties.Contains(propertyTypeAlias) == false) { - var convertAttempt = Properties[propertyTypeAlias].Value.TryConvertTo(); - return convertAttempt.Success ? convertAttempt.Result : default(TPassType); - } - catch (KeyNotFoundException ex) - { - var message = string.Format("Cannot find the property with alias '{0}' for the node named '{1}' with id '{2} that uses the document type alias '{3}'", - propertyTypeAlias, this.Name, this.Id, this.ContentTypeBase.Alias); + return default(TPassType); + } - throw new Exception(message, ex); - } + var convertAttempt = Properties[propertyTypeAlias].Value.TryConvertTo(); + return convertAttempt.Success ? convertAttempt.Result : default(TPassType); } /// From 73bc5c6ec92d208f7c992c0ba4c90600b3d72e1a Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 9 Aug 2017 15:29:12 +0200 Subject: [PATCH 553/714] changes needed for blueprints to differentiate them from content items. --- src/Umbraco.Core/Models/Content.cs | 5 +- src/Umbraco.Core/Models/IContent.cs | 8 +-- src/Umbraco.Core/Services/ContentService.cs | 40 +++++++------ src/Umbraco.Core/UdiEntityType.cs | 2 + src/Umbraco.Web/Editors/ContentController.cs | 56 +++++++++---------- .../ContentEditing/ContentItemDisplay.cs | 12 +--- .../Models/Mapping/ContentModelMapper.cs | 7 +-- 7 files changed, 59 insertions(+), 71 deletions(-) diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index d7db11dc3b..ef16a78248 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -1,8 +1,5 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Reflection; using System.Runtime.Serialization; @@ -333,7 +330,7 @@ namespace Umbraco.Core.Models //turn off change tracking clone.DisableChangeTracking(); //need to manually clone this since it's not settable - clone._contentType = (IContentType)ContentType.DeepClone(); + clone._contentType = (IContentType)ContentType.DeepClone(); //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); //re-enable tracking diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 3c92b17ae6..3d385f1b36 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -1,7 +1,5 @@ using System; using System.ComponentModel; -using System.Diagnostics; -using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Models { @@ -84,11 +82,11 @@ namespace Umbraco.Core.Models /// /// Gets the unique identifier of the published version, if any. /// - Guid PublishedVersionGuid { get; } - + Guid PublishedVersionGuid { get; } + /// /// Gets a value indicating whether the content item is a blueprint. - /// + /// bool IsBlueprint { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 60788eb958..1ba94105f5 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -13,13 +13,11 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; - using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; -using Umbraco.Core.Scoping; namespace Umbraco.Core.Services { @@ -1161,7 +1159,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); - var blueprint = repository.Get(id); + var blueprint = repository.Get(id); if (blueprint != null) ((Content) blueprint).IsBlueprint = true; return blueprint; @@ -1178,12 +1176,12 @@ namespace Umbraco.Core.Services var a = _idkMap.GetIdForKey(id, UmbracoObjectTypes.DocumentBlueprint); return a.Success ? GetBlueprintById(a.Result) : null; } - + public void SaveBlueprint(IContent content, int userId = 0) - { - //always ensure the blueprint is at the root + { + //always ensure the blueprint is at the root if (content.ParentId != -1) - content.ParentId = -1; + content.ParentId = -1; ((Content) content).IsBlueprint = true; @@ -1204,8 +1202,8 @@ namespace Umbraco.Core.Services } content.WriterId = userId; - repository.AddOrUpdate(content); - + repository.AddOrUpdate(content); + uow.Events.Dispatch(SavedBlueprint, this, new SaveEventArgs(content), "SavedBlueprint"); uow.Commit(); @@ -1220,7 +1218,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); - repository.Delete(content); + repository.Delete(content); uow.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), "DeletedBlueprint"); uow.Commit(); } @@ -1236,8 +1234,8 @@ namespace Umbraco.Core.Services content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id); content.CreatorId = userId; - content.WriterId = userId; - + content.WriterId = userId; + foreach (var property in blueprint.Properties) content.SetValue(property.Alias, property.Value); @@ -1683,21 +1681,21 @@ namespace Umbraco.Core.Services // Update the create author and last edit author copy.CreatorId = userId; copy.WriterId = userId; - + //get the current permissions, if there are any explicit ones they need to be copied var currentPermissions = GetPermissionsForEntity(content); - currentPermissions.RemoveWhere(p => p.IsDefaultPermissions); + currentPermissions.RemoveWhere(p => p.IsDefaultPermissions); repository.AddOrUpdate(copy); - repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - + repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + //add permissions if (currentPermissions.Count > 0) { var permissionSet = new ContentPermissionSet(copy, currentPermissions); - repository.AddOrUpdatePermissions(permissionSet); - } - + repository.AddOrUpdatePermissions(permissionSet); + } + uow.Commit(); // todo - this should flush, not commit //Special case for the associated tags @@ -2641,12 +2639,12 @@ namespace Umbraco.Core.Services /// Occurs after the Recycle Bin has been Emptied /// public static event TypedEventHandler EmptiedRecycleBin; - + /// /// Occurs after a blueprint has been saved. /// public static event TypedEventHandler> SavedBlueprint; - + /// /// Occurs after a blueprint has been deleted. /// diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs index 30c9f5513a..fcaf718813 100644 --- a/src/Umbraco.Core/UdiEntityType.cs +++ b/src/Umbraco.Core/UdiEntityType.cs @@ -44,6 +44,8 @@ namespace Umbraco.Core [UdiType(UdiType.GuidUdi)] public const string DocumentTypeContainer = "document-type-container"; [UdiType(UdiType.GuidUdi)] + public const string DocumentTypeBluePrints = "document-type-blueprints"; + [UdiType(UdiType.GuidUdi)] public const string MediaType = "media-type"; [UdiType(UdiType.GuidUdi)] public const string MediaTypeContainer = "media-type-container"; diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 838e5db493..c8e598016a 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -9,7 +9,6 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.ModelBinding; using AutoMapper; -using umbraco.BusinessLogic.Actions; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -81,8 +80,8 @@ namespace Umbraco.Web.Editors { var foundContent = Services.ContentService.GetByIds(ids); return foundContent.Select(Mapper.Map); - } - + } + /// /// Updates the permissions for a content item for a particular user group /// @@ -95,16 +94,16 @@ namespace Umbraco.Web.Editors public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) { if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - + var content = Services.ContentService.GetById(saveModel.ContentId); if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - + //current permissions explicitly assigned to this content item var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) - .ToDictionary(x => x.UserGroupId, x => x); + .ToDictionary(x => x.UserGroupId, x => x); + + var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); - var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); - //loop through each user group foreach (var userGroup in allUserGroups) { @@ -114,8 +113,8 @@ namespace Umbraco.Web.Editors { //create a string collection of the assigned letters var groupPermissionCodes = groupPermissions.ToArray(); - - //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions + + //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions //for this group/node which will go back to the defaults if (groupPermissionCodes.Length == 0) { @@ -141,8 +140,8 @@ namespace Umbraco.Web.Editors } return GetDetailedPermissions(content, allUserGroups); - } - + } + /// /// Returns the user group permissions for user groups assigned to this node /// @@ -166,10 +165,10 @@ namespace Umbraco.Web.Editors private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) { //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. - //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. + //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. + + var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); - var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); - var defaultPermissionsAsDictionary = defaultPermissionsByGroup .ToDictionary(x => Convert.ToInt32(x.Id), x => x); @@ -180,9 +179,9 @@ namespace Umbraco.Web.Editors foreach (var assignedGroupPermission in assignedPermissionsByGroup) { var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; - + //clone the default permissions model to the assigned ones - defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); + defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.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 @@ -192,10 +191,10 @@ namespace Umbraco.Web.Editors permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); } - } - + } + return defaultPermissionsByGroup; - } + } /// /// Returns an item to be used to display the recycle bin for content @@ -243,6 +242,7 @@ namespace Umbraco.Web.Editors content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); content.AllowedActions = new[] {"A"}; + content.IsBlueprint = true; var excludeProps = new[] {"_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"}; var propsTab = content.Tabs.Last(); @@ -428,7 +428,7 @@ namespace Umbraco.Web.Editors return Services.UserService .GetPermissions(Security.CurrentUser, nodeIds) .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); - } + } /// /// Checks a nodes permission for the current user @@ -501,7 +501,7 @@ namespace Umbraco.Web.Editors var contentItemDisplay = PostSaveInternal(contentItem, content => { - EnsureUniqueName(content.Name, content, "Name"); + EnsureUniqueName(content.Name, content, "Name"); Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); //we need to reuse the underlying logic so return the result that it wants @@ -522,7 +522,7 @@ namespace Umbraco.Web.Editors [ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { - return PostSaveInternal(contentItem, + return PostSaveInternal(contentItem, content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); } @@ -643,7 +643,7 @@ namespace Umbraco.Web.Editors display.PersistedContent = contentItem.PersistedContent; return display; - } + } /// /// Publishes a document with a given ID @@ -689,7 +689,7 @@ namespace Umbraco.Web.Editors return HandleContentNotFound(id, false); } - Services.ContentService.DeleteBlueprint(found); + Services.ContentService.DeleteBlueprint(found); return Request.CreateResponse(HttpStatusCode.OK); } @@ -1080,8 +1080,8 @@ namespace Umbraco.Web.Editors { return true; } - - //get the implicit/inherited permissions for the user for this path, + + //get the implicit/inherited permissions for the user for this path, //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) var path = contentItem != null ? contentItem.Path : nodeId.ToString(); var permission = userService.GetPermissionsForPath(user, path); @@ -1089,7 +1089,7 @@ namespace Umbraco.Web.Editors var allowed = true; foreach (var p in permissionsToCheck) { - if (permission == null + if (permission == null || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) { allowed = false; diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index c416f30a48..5d424e4dd3 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -1,15 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Net.Http.Formatting; using System.Runtime.Serialization; -using System.Web.Http; -using System.Web.Http.ModelBinding; -using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Core.Models.Validation; -using Umbraco.Web.Models.Trees; -using Umbraco.Web.Trees; namespace Umbraco.Web.Models.ContentEditing { @@ -57,6 +49,8 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "allowedActions")] public IEnumerable AllowedActions { get; set; } - + + [DataMember(Name = "isBlueprint")] + public bool IsBlueprint { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index 3b55def135..1809bffd3d 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.Linq; using System.Web; using System.Web.Mvc; -using System.Web.Routing; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; @@ -28,7 +27,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IContent TO ContentItemDisplay config.CreateMap() - .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Document, content.Key))) + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(content.IsBlueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, content.Key))) .ForMember(display => display.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(display => display.Updater, expression => expression.ResolveUsing(new CreatorResolver())) .ForMember(display => display.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -59,7 +58,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IContent TO ContentItemBasic config.CreateMap>() - .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Document, content.Key))) + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(content.IsBlueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, content.Key))) .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(dto => dto.Updater, expression => expression.ResolveUsing(new CreatorResolver())) .ForMember(dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -70,7 +69,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IContent TO ContentItemDto config.CreateMap>() - .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Document, content.Key))) + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(content.IsBlueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, content.Key))) .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(dto => dto.HasPublishedVersion, expression => expression.MapFrom(content => content.HasPublishedVersion)) .ForMember(dto => dto.Updater, expression => expression.Ignore()) From 4a21675a0abcbe026a49c0044732c33ee4f106fa Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 9 Aug 2017 23:42:31 +1000 Subject: [PATCH 554/714] removes null check on email since this will cause the log to fill up, we already check for null below anyways. --- .../HealthCheck/NotificationMethods/EmailNotificationMethod.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs index bfce87fde0..985ba90bc0 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs @@ -39,7 +39,6 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods : base(enabled, failureOnly, verbosity) { if (textService == null) throw new ArgumentNullException("textService"); - if (string.IsNullOrWhiteSpace(recipientEmail)) throw new ArgumentException("Value cannot be null or whitespace.", "recipientEmail"); _textService = textService; RecipientEmail = recipientEmail; Verbosity = verbosity; From 3263e496aa3d0ec0ecac0e9c9971ed0321071b80 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 00:02:27 +1000 Subject: [PATCH 555/714] U4-10201 Exception when deleting a user's start node - adds FKs, fixes delete scripts, adds migrations for FKs --- src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs | 2 + .../AddUserGroupTables.cs | 39 +++++++++++++++++++ .../Repositories/ContentRepository.cs | 4 +- .../Repositories/MediaRepository.cs | 2 + .../Repositories/RecycleBinRepository.cs | 20 +++++++++- .../Services/ContentServiceTests.cs | 9 +++++ 6 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs index fdad78bfcd..f6bb09066d 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -51,10 +51,12 @@ namespace Umbraco.Core.Models.Rdbms [Column("startContentId")] [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] public int? StartContentId { get; set; } [Column("startMediaId")] [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] public int? StartMediaId { get; set; } [ResultColumn] diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index d1cf0a40d9..36c45615a3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -28,6 +28,45 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe DeleteOldTables(tables, constraints); SetDefaultIcons(); } + else + { + //if we aren't adding the tables, make sure that the umbracoUserGroup table has the correct FKs - these + //were added after the beta release so we need to do some cleanup + //if the FK doesn't exist + if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUserGroup") + && x.Item2.InvariantEquals("startContentId") + && x.Item3.InvariantEquals("FK_startContentId_umbracoNode_id")) == false) + { + //before we add any foreign key we need to make sure there's no stale data in there which would have happened in the beta + //release if a start node was assigned and then that start node was deleted. + Execute.Sql(@"UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId NOT IN (SELECT id FROM umbracoNode)"); + + Create.ForeignKey("FK_startContentId_umbracoNode_id") + .FromTable("umbracoUserGroup") + .ForeignColumn("startContentId") + .ToTable("umbracoNode") + .PrimaryColumn("id") + .OnDelete(Rule.None) + .OnUpdate(Rule.None); + } + + if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUserGroup") + && x.Item2.InvariantEquals("startMediaId") + && x.Item3.InvariantEquals("FK_startMediaId_umbracoNode_id")) == false) + { + //before we add any foreign key we need to make sure there's no stale data in there which would have happened in the beta + //release if a start node was assigned and then that start node was deleted. + Execute.Sql(@"UPDATE umbracoUserGroup SET startMediaId = NULL WHERE startMediaId NOT IN (SELECT id FROM umbracoNode)"); + + Create.ForeignKey("FK_startMediaId_umbracoNode_id") + .FromTable("umbracoUserGroup") + .ForeignColumn("startMediaId") + .ToTable("umbracoNode") + .PrimaryColumn("id") + .OnDelete(Rule.None) + .OnUpdate(Rule.None); + } + } } private void SetDefaultIcons() diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index addd0bc925..daa8140670 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -183,6 +183,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", + "UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", "DELETE FROM umbracoRelation WHERE childId = @Id", "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", @@ -194,7 +196,7 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" + "DELETE FROM umbracoNode WHERE id = @Id" }; return list; } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index ee413c2a59..be96d80f5a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -118,6 +118,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", + "UPDATE umbracoUserGroup SET startMediaId = NULL WHERE startMediaId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", "DELETE FROM umbracoRelation WHERE childId = @Id", "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index efb7487fdf..efb93bcff3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -53,7 +53,10 @@ namespace Umbraco.Core.Persistence.Repositories @"DELETE FROM umbracoRedirectUrl WHERE umbracoRedirectUrl.id IN( SELECT TB1.id FROM umbracoRedirectUrl as TB1 INNER JOIN umbracoNode as TB2 ON TB1.contentKey = TB2.uniqueId - WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + FormatDeleteStatement("umbracoUserStartNode", "startNode"), + FormatUpdateStatement("umbracoUserGroup", "startContentId"), + FormatUpdateStatement("umbracoUserGroup", "startMediaId"), FormatDeleteStatement("umbracoRelation", "parentId"), FormatDeleteStatement("umbracoRelation", "childId"), FormatDeleteStatement("cmsTagRelationship", "nodeId"), @@ -108,6 +111,21 @@ namespace Umbraco.Core.Persistence.Repositories tableName, keyName); } + /// + /// An update statement that will update a value to NULL in the table specified where its PK (keyName) is found in the + /// list of umbracoNode.id that have trashed flag set + /// + /// + /// + /// + private string FormatUpdateStatement(string tableName, string keyName) + { + return + string.Format( + "UPDATE {0} SET {0}.{1} = NULL WHERE {0}.{1} IN (SELECT id FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType)", + tableName, keyName); + } + /// /// Gets a list of files, which are referenced on items in the Recycle Bin. /// The list is generated by the convention that a file is referenced by diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 5a2b9f23e7..c9daeb0022 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1583,6 +1583,14 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.Save(content2, 0); Assert.IsTrue(ServiceContext.ContentService.PublishWithStatus(content2, 0).Success); + var editorGroup = ServiceContext.UserService.GetUserGroupByAlias("editor"); + editorGroup.StartContentId = content1.Id; + ServiceContext.UserService.Save(editorGroup); + + var admin = ServiceContext.UserService.GetUserById(0); + admin.StartContentIds = new[] {content1.Id}; + ServiceContext.UserService.Save(admin); + ServiceContext.RelationService.Save(new RelationType(Constants.ObjectTypes.DocumentGuid, Constants.ObjectTypes.DocumentGuid, "test")); Assert.IsNotNull(ServiceContext.RelationService.Relate(content1, content2, "test")); @@ -1608,6 +1616,7 @@ namespace Umbraco.Tests.Services }).Success); // Act + ServiceContext.ContentService.MoveToRecycleBin(content1); ServiceContext.ContentService.EmptyRecycleBin(); var contents = ServiceContext.ContentService.GetContentInRecycleBin(); From 0bb27b8ebd1aba7b54866d9075d2f1efe11f6c97 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 00:22:15 +1000 Subject: [PATCH 556/714] U4-10263 Missing UDI + GUID friendly MediaService.GetByIds, ContentService.GetByIds --- src/Umbraco.Core/Services/ContentService.cs | 20 ++++++++++++++++ .../Services/ContentServiceExtensions.cs | 24 ++++++++++++++++++- src/Umbraco.Core/Services/IContentService.cs | 16 +++++++++++++ src/Umbraco.Core/Services/IMediaService.cs | 16 +++++++++++++ src/Umbraco.Core/Services/MediaService.cs | 22 ++++++++++++++++- .../Services/MediaServiceExtensions.cs | 24 +++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + 7 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Core/Services/MediaServiceExtensions.cs diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 60788eb958..b635caef5b 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -143,6 +143,26 @@ namespace Umbraco.Core.Services } } + /// + /// Creates an object using the alias of the + /// that this Content should based on. + /// + /// + /// Note that using this method will simply return a new IContent without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new content objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Content object + /// Id of Parent for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + public IContent CreateContent(string name, Guid parentId, string contentTypeAlias, int userId = 0) + { + var parent = GetById(parentId); + return CreateContent(name, parent, contentTypeAlias, userId); + } + /// /// Creates an object using the alias of the /// that this Content should based on. diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index b3380cf1bf..d7f9c52d38 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models; @@ -8,8 +9,29 @@ using Umbraco.Core.Persistence.Repositories; namespace Umbraco.Core.Services { + /// + /// Content service extension methods + /// public static class ContentServiceExtensions - { + { + /// + /// Method to create an IContent object based on the Udi of a parent + /// + /// + /// + /// + /// + /// + /// + public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string mediaTypeAlias, int userId = 0) + { + var guidUdi = parentId as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content"); + var parent = contentService.GetById(guidUdi.Guid); + return contentService.CreateContent(name, parent, mediaTypeAlias, userId); + } + /// /// Remove all permissions for this user for all nodes /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index e33f46e3f4..4caa295659 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -162,6 +162,22 @@ namespace Umbraco.Core.Services IEnumerable GetByIds(IEnumerable ids); + /// + /// Creates an object using the alias of the + /// that this Content should based on. + /// + /// + /// Note that using this method will simply return a new IContent without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new content objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Content object + /// Id of Parent for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + IContent CreateContent(string name, Guid parentId, string contentTypeAlias, int userId = 0); + /// /// Creates an object using the alias of the /// that this Content should based on. diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 8cd2b0eed5..8ef126c198 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -88,6 +88,22 @@ namespace Umbraco.Core.Services IEnumerable GetByIds(IEnumerable ids); + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = 0); + /// /// Creates an object using the alias of the /// that this Media should based on. diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 38c0c20eb2..6476e9a86d 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -45,7 +45,27 @@ namespace Umbraco.Core.Services _dataTypeService = dataTypeService; _userService = userService; _idkMap = idkMap; - } + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = 0) + { + var parent = GetById(parentId); + return CreateMedia(name, parent, mediaTypeAlias, userId); + } /// /// Creates an object using the alias of the diff --git a/src/Umbraco.Core/Services/MediaServiceExtensions.cs b/src/Umbraco.Core/Services/MediaServiceExtensions.cs new file mode 100644 index 0000000000..3e3dea1a4a --- /dev/null +++ b/src/Umbraco.Core/Services/MediaServiceExtensions.cs @@ -0,0 +1,24 @@ +using System; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + /// + /// Media service extension methods + /// + /// + /// Many of these have to do with UDI lookups but we don't need to add these methods to the service interface since a UDI is just a GUID + /// and the services already support GUIDs + /// + public static class MediaServiceExtensions + { + public static IMedia CreateMedia(this IMediaService mediaService, string name, Udi parentId, string mediaTypeAlias, int userId = 0) + { + var guidUdi = parentId as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by media"); + var parent = mediaService.GetById(guidUdi.Guid); + return mediaService.CreateMedia(name, parent, mediaTypeAlias, userId); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 96183f61bf..a676d8caa3 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -705,6 +705,7 @@ + From 558a2847506b27d792b1584b6535eb49aa32c634 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 01:01:07 +1000 Subject: [PATCH 557/714] U4-10263 Missing UDI + GUID friendly MediaService.GetByIds, ContentService.GetByIds --- .../Repositories/ContentRepository.cs | 102 ++++++++++++++++- .../Interfaces/IContentRepository.cs | 2 +- .../Interfaces/IMediaRepository.cs | 3 +- .../Repositories/MediaRepository.cs | 108 ++++++++++++++++++ src/Umbraco.Core/Services/ContentService.cs | 30 ++++- src/Umbraco.Core/Services/IContentService.cs | 1 + src/Umbraco.Core/Services/IMediaService.cs | 1 + src/Umbraco.Core/Services/MediaService.cs | 18 ++- .../Repositories/ContentRepositoryTest.cs | 10 ++ .../Repositories/MediaRepositoryTest.cs | 10 ++ 10 files changed, 279 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index addd0bc925..821af8966e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -31,6 +31,7 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ContentPreviewRepository _contentPreviewRepository; private readonly ContentXmlRepository _contentXmlRepository; private readonly PermissionRepository _permissionRepository; + private readonly ContentByGuidReadRepository _contentByGuidReadRepository; public ContentRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cacheHelper, logger, syntaxProvider, contentSection) @@ -44,7 +45,7 @@ namespace Umbraco.Core.Persistence.Repositories _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, syntaxProvider); _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, syntaxProvider); _permissionRepository = new PermissionRepository(UnitOfWork, cacheHelper, Logger, SqlSyntax); - + _contentByGuidReadRepository = new ContentByGuidReadRepository(this, work, cacheHelper, logger, syntaxProvider); EnsureUniqueNaming = true; } @@ -938,6 +939,105 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", #endregion + #region Read Repository implementation for GUID keys + public IContent Get(Guid id) + { + return _contentByGuidReadRepository.Get(id); + } + + IEnumerable IReadRepository.GetAll(params Guid[] ids) + { + return _contentByGuidReadRepository.GetAll(ids); + } + + public bool Exists(Guid id) + { + return _contentByGuidReadRepository.Exists(id); + } + + /// + /// A reading repository purely for looking up by GUID + /// + /// + /// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + /// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them + /// + private class ContentByGuidReadRepository : PetaPocoRepositoryBase + { + private readonly ContentRepository _outerRepo; + + public ContentByGuidReadRepository(ContentRepository outerRepo, + IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + _outerRepo = outerRepo; + } + + protected override IContent PerformGet(Guid id) + { + var sql = _outerRepo.GetBaseQuery(BaseQueryType.FullSingle) + .Where(GetBaseWhereClause(), new { Id = id }) + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + + if (dto == null) + return null; + + var content = _outerRepo.CreateContentFromDto(dto, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + var sql = GetBaseQuery(false); + if (ids.Any()) + { + sql.Where("umbracoNode.uniqueID in (@ids)", new { ids = ids }); + } + + return _outerRepo.ProcessQuery(sql, new PagingSqlQuery(sql)); + } + + protected override Sql GetBaseQuery(bool isCount) + { + return _outerRepo.GetBaseQuery(isCount); + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.uniqueID = @Id"; + } + + protected override Guid NodeObjectTypeId + { + get { return _outerRepo.NodeObjectTypeId; } + } + + #region Not needed to implement + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + protected override IEnumerable GetDeleteClauses() + { + throw new NotImplementedException(); + } + protected override void PersistNewItem(IContent entity) + { + throw new NotImplementedException(); + } + protected override void PersistUpdatedItem(IContent entity) + { + throw new NotImplementedException(); + } + #endregion + } + #endregion + protected override string GetDatabaseFieldNameForOrderBy(string orderBy) { //Some custom ones diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index c019b694d2..c98f073f40 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository + public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IReadRepository, IDeleteMediaFilesRepository { /// /// This builds the Xml document used for the XML cache diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 4c9b1d3561..b6a22ba761 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -7,9 +7,8 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository + public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IReadRepository, IDeleteMediaFilesRepository { - /// /// Used to add/update published xml for the media item /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index ee413c2a59..828ae843f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -27,6 +27,7 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ITagRepository _tagRepository; private readonly ContentXmlRepository _contentXmlRepository; private readonly ContentPreviewRepository _contentPreviewRepository; + private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; public MediaRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cache, logger, sqlSyntax, contentSection) @@ -37,6 +38,7 @@ namespace Umbraco.Core.Persistence.Repositories _tagRepository = tagRepository; _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, sqlSyntax); _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, sqlSyntax); + _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, work, cache, logger, sqlSyntax); EnsureUniqueNaming = contentSection.EnsureUniqueNaming; } @@ -522,6 +524,112 @@ namespace Umbraco.Core.Persistence.Repositories #endregion + #region Read Repository implementation for GUID keys + public IMedia Get(Guid id) + { + return _mediaByGuidReadRepository.Get(id); + } + + IEnumerable IReadRepository.GetAll(params Guid[] ids) + { + return _mediaByGuidReadRepository.GetAll(ids); + } + + public bool Exists(Guid id) + { + return _mediaByGuidReadRepository.Exists(id); + } + + /// + /// A reading repository purely for looking up by GUID + /// + /// + /// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + /// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them + /// + private class MediaByGuidReadRepository : PetaPocoRepositoryBase + { + private readonly MediaRepository _outerRepo; + + public MediaByGuidReadRepository(MediaRepository outerRepo, + IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + _outerRepo = outerRepo; + } + + protected override IMedia PerformGet(Guid id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.OrderByDescending(x => x.VersionDate, SqlSyntax); + + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + + if (dto == null) + return null; + + var content = _outerRepo.CreateMediaFromDto(dto, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + Func translate = s => + { + if (ids.Any()) + { + s.Where("umbracoNode.uniqueID in (@ids)", new { ids }); + } + //we only want the newest ones with this method + s.Where(x => x.Newest, SqlSyntax); + return s; + }; + + var sqlBaseFull = _outerRepo.GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = _outerRepo.GetBaseQuery(BaseQueryType.Ids); + + return _outerRepo.ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); + } + + protected override Sql GetBaseQuery(bool isCount) + { + return _outerRepo.GetBaseQuery(isCount); + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.uniqueID = @Id"; + } + + protected override Guid NodeObjectTypeId + { + get { return _outerRepo.NodeObjectTypeId; } + } + + #region Not needed to implement + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + protected override IEnumerable GetDeleteClauses() + { + throw new NotImplementedException(); + } + protected override void PersistNewItem(IMedia entity) + { + throw new NotImplementedException(); + } + protected override void PersistUpdatedItem(IMedia entity) + { + throw new NotImplementedException(); + } + #endregion + } + #endregion + /// /// Gets paged media results /// diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index b635caef5b..e6f402de46 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -369,7 +369,7 @@ namespace Umbraco.Core.Services } /// - /// Gets an object by Id + /// Gets an objects by Ids /// /// Ids of the Content to retrieve /// @@ -396,6 +396,34 @@ namespace Umbraco.Core.Services } } + /// + /// Gets an objects by Ids + /// + /// Ids of the Content to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + var idsArray = ids.ToArray(); + if (idsArray.Length == 0) return Enumerable.Empty(); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + // ensure that the result has the order based on the ids passed in + var result = repository.GetAll(idsArray); + var content = result.ToDictionary(x => x.Key, x => x); + + var sortedResult = idsArray.Select(x => + { + IContent c; + return content.TryGetValue(x, out c) ? c : null; + }).WhereNotNull(); + + return sortedResult; + } + } + /// /// Gets an object by its 'UniqueId' /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 4caa295659..7a5c64c0c0 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -161,6 +161,7 @@ namespace Umbraco.Core.Services bool SendToPublication(IContent content, int userId = 0); IEnumerable GetByIds(IEnumerable ids); + IEnumerable GetByIds(IEnumerable ids); /// /// Creates an object using the alias of the diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 8ef126c198..04683e5747 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -87,6 +87,7 @@ namespace Umbraco.Core.Services int CountDescendants(int parentId, string contentTypeAlias = null); IEnumerable GetByIds(IEnumerable ids); + IEnumerable GetByIds(IEnumerable ids); /// /// Creates an object using the alias of the diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 6476e9a86d..28b1275823 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -327,7 +327,7 @@ namespace Umbraco.Core.Services } /// - /// Gets an object by Id + /// Gets an objects by Ids /// /// Ids of the Media to retrieve /// @@ -342,6 +342,22 @@ namespace Umbraco.Core.Services } } + /// + /// Gets an objects by Ids + /// + /// Ids of the Media to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + if (ids.Any() == false) return Enumerable.Empty(); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateMediaRepository(uow); + return repository.GetAll(ids.ToArray()); + } + } + /// /// Gets an object by its 'UniqueId' /// diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index f969488055..9301efa68a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -1039,6 +1039,16 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(contents, Is.Not.Null); Assert.That(contents.Any(), Is.True); Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4)); + + contents = repository.GetAll(contents.Select(x => x.Id).ToArray()); + Assert.That(contents, Is.Not.Null); + Assert.That(contents.Any(), Is.True); + Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4)); + + contents = ((IReadRepository)repository).GetAll(contents.Select(x => x.Key).ToArray()); + Assert.That(contents, Is.Not.Null); + Assert.That(contents.Any(), Is.True); + Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4)); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index c9f78fb201..e0ad754195 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -627,6 +627,16 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(medias, Is.Not.Null); Assert.That(medias.Any(), Is.True); Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3)); + + medias = repository.GetAll(medias.Select(x => x.Id).ToArray()); + Assert.That(medias, Is.Not.Null); + Assert.That(medias.Any(), Is.True); + Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(4)); + + medias = ((IReadRepository)repository).GetAll(medias.Select(x => x.Key).ToArray()); + Assert.That(medias, Is.Not.Null); + Assert.That(medias.Any(), Is.True); + Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(4)); } } From 8f121c4d93d38fafce893558e40fde3944648142 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 01:01:34 +1000 Subject: [PATCH 558/714] update test --- .../Persistence/Repositories/MediaRepositoryTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index e0ad754195..4dfaccd7ab 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -631,12 +631,12 @@ namespace Umbraco.Tests.Persistence.Repositories medias = repository.GetAll(medias.Select(x => x.Id).ToArray()); Assert.That(medias, Is.Not.Null); Assert.That(medias.Any(), Is.True); - Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(4)); + Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3)); medias = ((IReadRepository)repository).GetAll(medias.Select(x => x.Key).ToArray()); Assert.That(medias, Is.Not.Null); Assert.That(medias.Any(), Is.True); - Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(4)); + Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3)); } } From 2c219576f6188b960b32a025a27e96cd1b7b0f45 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 01:08:29 +1000 Subject: [PATCH 559/714] update test --- .../Repositories/ContentRepository.cs | 18 +++++++++++++----- .../Repositories/MediaRepository.cs | 18 +++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 821af8966e..236f0ed311 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -992,13 +992,21 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", protected override IEnumerable PerformGetAll(params Guid[] ids) { - var sql = GetBaseQuery(false); - if (ids.Any()) + Func translate = s => { - sql.Where("umbracoNode.uniqueID in (@ids)", new { ids = ids }); - } + if (ids.Any()) + { + s.Where("umbracoNode.uniqueID in (@ids)", new { ids }); + } + //we only want the newest ones with this method + s.Where(x => x.Newest, SqlSyntax); + return s; + }; - return _outerRepo.ProcessQuery(sql, new PagingSqlQuery(sql)); + var sqlBaseFull = _outerRepo.GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = _outerRepo.GetBaseQuery(BaseQueryType.Ids); + + return _outerRepo.ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); } protected override Sql GetBaseQuery(bool isCount) diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 828ae843f8..ad2dd26a43 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -576,21 +576,13 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformGetAll(params Guid[] ids) { - Func translate = s => + var sql = GetBaseQuery(false); + if (ids.Any()) { - if (ids.Any()) - { - s.Where("umbracoNode.uniqueID in (@ids)", new { ids }); - } - //we only want the newest ones with this method - s.Where(x => x.Newest, SqlSyntax); - return s; - }; + sql.Where("umbracoNode.uniqueID in (@ids)", new { ids = ids }); + } - var sqlBaseFull = _outerRepo.GetBaseQuery(BaseQueryType.FullMultiple); - var sqlBaseIds = _outerRepo.GetBaseQuery(BaseQueryType.Ids); - - return _outerRepo.ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); + return _outerRepo.ProcessQuery(sql, new PagingSqlQuery(sql)); } protected override Sql GetBaseQuery(bool isCount) From bdf621049f2fb9d824ee3f92c102cd2af2277655 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 01:11:59 +1000 Subject: [PATCH 560/714] adds GetByIds for UDIs --- .../Services/ContentServiceExtensions.cs | 14 ++++++++++++++ .../Services/MediaServiceExtensions.cs | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index d7f9c52d38..6e3f75c42d 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -14,6 +14,20 @@ namespace Umbraco.Core.Services /// public static class ContentServiceExtensions { + public static IEnumerable GetByIds(this IContentService contentService, IEnumerable ids) + { + var guids = new List(); + foreach (var udi in ids) + { + var guidUdi = udi as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content"); + guids.Add(guidUdi); + } + + return contentService.GetByIds(guids.Select(x => x.Guid)); + } + /// /// Method to create an IContent object based on the Udi of a parent /// diff --git a/src/Umbraco.Core/Services/MediaServiceExtensions.cs b/src/Umbraco.Core/Services/MediaServiceExtensions.cs index 3e3dea1a4a..41e0ebe55e 100644 --- a/src/Umbraco.Core/Services/MediaServiceExtensions.cs +++ b/src/Umbraco.Core/Services/MediaServiceExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Models; namespace Umbraco.Core.Services @@ -12,6 +14,20 @@ namespace Umbraco.Core.Services /// public static class MediaServiceExtensions { + public static IEnumerable GetByIds(this IMediaService mediaService, IEnumerable ids) + { + var guids = new List(); + foreach (var udi in ids) + { + var guidUdi = udi as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by media"); + guids.Add(guidUdi); + } + + return mediaService.GetByIds(guids.Select(x => x.Guid)); + } + public static IMedia CreateMedia(this IMediaService mediaService, string name, Udi parentId, string mediaTypeAlias, int userId = 0) { var guidUdi = parentId as GuidUdi; From a97a5dc3fdcf85c3e791530a0fd2d225e74fd002 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 10:52:31 +1000 Subject: [PATCH 561/714] Removes need for the IdkMap for content/media services since we have a wrapping repo now, adds tests to verify that caching is active for both INT and GUID --- src/Umbraco.Core/Services/ContentService.cs | 32 ++++----- src/Umbraco.Core/Services/MediaService.cs | 18 ++--- src/Umbraco.Core/Services/ServiceContext.cs | 4 +- .../Repositories/ContentRepositoryTest.cs | 67 ++++++++++++++++--- .../Repositories/MediaRepositoryTest.cs | 57 ++++++++++++++-- 5 files changed, 131 insertions(+), 47 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index e6f402de46..49d745f0cf 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -32,7 +32,6 @@ namespace Umbraco.Core.Services private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; - private readonly IdkMap _idkMap; //Support recursive locks because some of the methods that require locking call other methods that require locking. //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. @@ -44,8 +43,7 @@ namespace Umbraco.Core.Services ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, - IUserService userService, - IdkMap idkMap) + IUserService userService) : base(provider, repositoryFactory, logger, eventMessagesFactory) { if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); @@ -53,7 +51,6 @@ namespace Umbraco.Core.Services _publishingStrategy = new PublishingStrategy(UowProvider.ScopeProvider, eventMessagesFactory, logger); _dataTypeService = dataTypeService; _userService = userService; - _idkMap = idkMap; } #region Static Queries @@ -431,13 +428,11 @@ namespace Umbraco.Core.Services /// public IContent GetById(Guid key) { - // the repository implements a cache policy on int identifiers, not guids, - // and we are not changing it now, but we still would like to rely on caching - // instead of running a full query against the database, so relying on the - // id-key map, which is fast. - - var a = _idkMap.GetIdForKey(key, UmbracoObjectTypes.Document); - return a.Success ? GetById(a.Result) : null; + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.Get(key); + } } /// @@ -1218,13 +1213,14 @@ namespace Umbraco.Core.Services public IContent GetBlueprintById(Guid id) { - // the repository implements a cache policy on int identifiers, not guids, - // and we are not changing it now, but we still would like to rely on caching - // instead of running a full query against the database, so relying on the - // id-key map, which is fast. - - var a = _idkMap.GetIdForKey(id, UmbracoObjectTypes.DocumentBlueprint); - return a.Success ? GetBlueprintById(a.Result) : null; + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + var blueprint = repository.Get(id); + if (blueprint != null) + ((Content)blueprint).IsBlueprint = true; + return blueprint; + } } public void SaveBlueprint(IContent content, int userId = 0) diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 28b1275823..ee4cddb382 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -35,16 +35,14 @@ namespace Umbraco.Core.Services private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; private readonly MediaFileSystem _mediaFileSystem = FileSystemProviderManager.Current.MediaFileSystem; - private readonly IdkMap _idkMap; - public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService, IdkMap idkMap) + public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) : base(provider, repositoryFactory, logger, eventMessagesFactory) { if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); if (userService == null) throw new ArgumentNullException("userService"); _dataTypeService = dataTypeService; _userService = userService; - _idkMap = idkMap; } /// @@ -365,15 +363,11 @@ namespace Umbraco.Core.Services /// public IMedia GetById(Guid key) { - // the repository implements a cache policy on int identifiers, not guids, - // and we are not changing it now, but we still would like to rely on caching - // instead of running a full query against the database, so relying on the - // id-key map, which is fast. - // - // we should inject the id-key map but ... breaking changes ... yada - - var a = _idkMap.GetIdForKey(key, UmbracoObjectTypes.Media); - return a.Success ? GetById(a.Result) : null; + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateMediaRepository(uow); + return repository.Get(key); + } } /// diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 3da10e132b..144d548b78 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -261,10 +261,10 @@ namespace Umbraco.Core.Services _memberService = new Lazy(() => new MemberService(provider, repositoryFactory, logger, eventMessagesFactory, _memberGroupService.Value, _dataTypeService.Value)); if (_contentService == null) - _contentService = new Lazy(() => new ContentService(provider, repositoryFactory, logger, eventMessagesFactory, _dataTypeService.Value, _userService.Value, idkMap)); + _contentService = new Lazy(() => new ContentService(provider, repositoryFactory, logger, eventMessagesFactory, _dataTypeService.Value, _userService.Value)); if (_mediaService == null) - _mediaService = new Lazy(() => new MediaService(provider, repositoryFactory, logger, eventMessagesFactory, _dataTypeService.Value, _userService.Value, idkMap)); + _mediaService = new Lazy(() => new MediaService(provider, repositoryFactory, logger, eventMessagesFactory, _dataTypeService.Value, _userService.Value)); if (_contentTypeService == null) _contentTypeService = new Lazy(() => new ContentTypeService(provider, repositoryFactory, logger, eventMessagesFactory, _contentService.Value, _mediaService.Value)); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 9301efa68a..8814ef229e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Web; using System.Xml.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -41,32 +43,75 @@ namespace Umbraco.Tests.Persistence.Repositories base.TearDown(); } - private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out DataTypeDefinitionRepository dtdRepository) + private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out DataTypeDefinitionRepository dtdRepository, CacheHelper cacheHelper = null) { TemplateRepository tr; var ctRepository = CreateRepository(unitOfWork, out contentTypeRepository, out tr); - dtdRepository = new DataTypeDefinitionRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository); + dtdRepository = new DataTypeDefinitionRepository(unitOfWork, cacheHelper, Logger, SqlSyntax, contentTypeRepository); return ctRepository; } - private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository) + private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, CacheHelper cacheHelper = null) { TemplateRepository tr; - return CreateRepository(unitOfWork, out contentTypeRepository, out tr); + return CreateRepository(unitOfWork, out contentTypeRepository, out tr, cacheHelper); } - private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out TemplateRepository templateRepository) + private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out TemplateRepository templateRepository, CacheHelper cacheHelper = null) { - templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); - var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); - contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); - var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + cacheHelper = cacheHelper ?? CacheHelper; + + templateRepository = new TemplateRepository(unitOfWork, cacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); + var tagRepository = new TagRepository(unitOfWork, cacheHelper, Logger, SqlSyntax); + contentTypeRepository = new ContentTypeRepository(unitOfWork, cacheHelper, Logger, SqlSyntax, templateRepository); + var repository = new ContentRepository(unitOfWork, cacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); return repository; } - private UserGroupRepository CreateUserGroupRepository(IScopeUnitOfWork unitOfWork) + [Test] + public void Cache_Active_By_Int_And_Guid() { - return new UserGroupRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + + var realCache = new CacheHelper( + new ObjectCacheRuntimeCacheProvider(), + new StaticCacheProvider(), + new StaticCacheProvider(), + new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider())); + + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository, cacheHelper: realCache)) + { + DatabaseContext.Database.DisableSqlCount(); + + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + var content = MockedContent.CreateSimpleContent(contentType); + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(content); + unitOfWork.Commit(); + + DatabaseContext.Database.EnableSqlCount(); + + //go get it, this should already be cached since the default repository key is the INT + var found = repository.Get(content.Id); + Assert.AreEqual(0, DatabaseContext.Database.SqlCount); + //retrieve again, this should use cache + found = repository.Get(content.Id); + Assert.AreEqual(0, DatabaseContext.Database.SqlCount); + + //reset counter + DatabaseContext.Database.DisableSqlCount(); + DatabaseContext.Database.EnableSqlCount(); + + //now get by GUID, this won't be cached yet because the default repo key is not a GUID + found = repository.Get(content.Key); + var sqlCount = DatabaseContext.Database.SqlCount; + Assert.Greater(sqlCount, 0); + //retrieve again, this should use cache now + found = repository.Get(content.Key); + Assert.AreEqual(sqlCount, DatabaseContext.Database.SqlCount); + } } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 4dfaccd7ab..a2f82b0ad0 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -5,6 +5,7 @@ using System.Xml.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -35,14 +36,62 @@ namespace Umbraco.Tests.Persistence.Repositories CreateTestData(); } - private MediaRepository CreateRepository(IScopeUnitOfWork unitOfWork, out MediaTypeRepository mediaTypeRepository) + private MediaRepository CreateRepository(IScopeUnitOfWork unitOfWork, out MediaTypeRepository mediaTypeRepository, CacheHelper cacheHelper = null) { - mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); - var tagRepository = new TagRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); - var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); + cacheHelper = cacheHelper ?? CacheHelper; + + mediaTypeRepository = new MediaTypeRepository(unitOfWork, cacheHelper, Mock.Of(), SqlSyntax); + var tagRepository = new TagRepository(unitOfWork, cacheHelper, Mock.Of(), SqlSyntax); + var repository = new MediaRepository(unitOfWork, cacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); return repository; } + [Test] + public void Cache_Active_By_Int_And_Guid() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + + var realCache = new CacheHelper( + new ObjectCacheRuntimeCacheProvider(), + new StaticCacheProvider(), + new StaticCacheProvider(), + new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider())); + + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository, cacheHelper: realCache)) + { + DatabaseContext.Database.DisableSqlCount(); + + var mediaType = MockedContentTypes.CreateSimpleMediaType("umbTextpage1", "Textpage"); + var media = MockedMedia.CreateSimpleMedia(mediaType, "hello", -1); + mediaTypeRepository.AddOrUpdate(mediaType); + repository.AddOrUpdate(media); + unitOfWork.Commit(); + + DatabaseContext.Database.EnableSqlCount(); + + //go get it, this should already be cached since the default repository key is the INT + var found = repository.Get(media.Id); + Assert.AreEqual(0, DatabaseContext.Database.SqlCount); + //retrieve again, this should use cache + found = repository.Get(media.Id); + Assert.AreEqual(0, DatabaseContext.Database.SqlCount); + + //reset counter + DatabaseContext.Database.DisableSqlCount(); + DatabaseContext.Database.EnableSqlCount(); + + //now get by GUID, this won't be cached yet because the default repo key is not a GUID + found = repository.Get(media.Key); + var sqlCount = DatabaseContext.Database.SqlCount; + Assert.Greater(sqlCount, 0); + //retrieve again, this should use cache now + found = repository.Get(media.Key); + Assert.AreEqual(sqlCount, DatabaseContext.Database.SqlCount); + } + } + [Test] public void Rebuild_All_Xml_Structures() { From d756f884a7622f829a4739b4650f485bfacb06b3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 12:02:23 +1000 Subject: [PATCH 562/714] fixes test --- .../Persistence/Repositories/ContentRepositoryTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 8814ef229e..22331bc850 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -45,6 +45,8 @@ namespace Umbraco.Tests.Persistence.Repositories private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out DataTypeDefinitionRepository dtdRepository, CacheHelper cacheHelper = null) { + cacheHelper = cacheHelper ?? CacheHelper; + TemplateRepository tr; var ctRepository = CreateRepository(unitOfWork, out contentTypeRepository, out tr); dtdRepository = new DataTypeDefinitionRepository(unitOfWork, cacheHelper, Logger, SqlSyntax, contentTypeRepository); From 72495b3eaf2a34cd87653a4b5ee79996f7e9beda Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 17:25:16 +1000 Subject: [PATCH 563/714] U4-10227 Users section should start in the user tree --- .../application/umbsections.directive.js | 12 ++- src/Umbraco.Web.UI.Client/src/routes.js | 33 ++++--- .../Editors/DashboardController.cs | 51 +--------- src/Umbraco.Web/Editors/DashboardHelper.cs | 96 +++++++++++++++++++ src/Umbraco.Web/Editors/SectionController.cs | 44 ++++++++- .../Models/ContentEditing/Section.cs | 7 ++ .../Trees/ApplicationTreeController.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 8 files changed, 178 insertions(+), 68 deletions(-) create mode 100644 src/Umbraco.Web/Editors/DashboardHelper.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js index 5ce3eac8a0..6183151c2a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js @@ -154,8 +154,16 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se } navigationService.hideSearch(); - navigationService.showTree(section.alias); - $location.path("/" + section.alias); + navigationService.showTree(section.alias); + + //in some cases the section will have a custom route path specified, if there is one we'll use it + if (section.routePath) { + $location.path(section.routePath); + } + else { + $location.path(section.alias); + } + }; scope.sectionDblClick = function(section){ diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 69e6779a15..7eafd7106d 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -98,14 +98,25 @@ app.config(function ($routeProvider) { resolve: doLogout() }) .when('/:section', { - templateUrl: function (rp) { - if (rp.section.toLowerCase() === "default" || rp.section.toLowerCase() === "umbraco" || rp.section === "") - { - rp.section = "content"; + //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. + template: "
", + //This controller will execute for this route, then we can execute some code in order to set the template Url + controller: function ($scope, $route, $routeParams, $location) { + if ($routeParams.section.toLowerCase() === "default" || $routeParams.section.toLowerCase() === "umbraco" || $routeParams.section === "") { + $routeParams.section = "content"; } + + //TODO: Here we could run some extra logic to check if the dashboard we are navigating + //to has any content to show and if not it could redirect to the first tree root path. + //BUT! this would mean that we'd need a server side call to check this data.... + //Instead we already have this data in the sections returned from the sectionResource but we + //don't want to cache data in a resource so we'd have to create a sectionService which would rely + //on the userService, then we update the umbsections.directive to use the sectionService and when the + //sectionService requests the sections, it caches the result against the current user. Then we can + //use the sectionService here to do the redirection. - rp.url = "dashboard.aspx?app=" + rp.section; - return 'views/common/dashboard.html'; + $routeParams.url = "dashboard.aspx?app=" + $routeParams.section; + $scope.templateUrl = 'views/common/dashboard.html'; }, resolve: canRoute(true) }) @@ -121,15 +132,11 @@ app.config(function ($routeProvider) { }) .when('/:section/:tree/:method', { templateUrl: function (rp) { + + //if there is no method registered for this then show the dashboard if (!rp.method) return "views/common/dashboard.html"; - - //NOTE: This current isn't utilized by anything but does open up some cool opportunities for - // us since we'll be able to have specialized views for individual sections which is something - // we've never had before. So could utilize this for a new dashboard model when we get native - // angular dashboards working. Perhaps a normal section dashboard would list out the registered - // dashboards (as tabs if we wanted) and each tab could actually be a route link to one of these views? - + return ('views/' + rp.tree + '/' + rp.method + '.html'); }, resolve: canRoute(true) diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index e4c94965b7..55286d99b4 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -19,7 +19,6 @@ using Umbraco.Core.Logging; namespace Umbraco.Web.Editors { - //we need to fire up the controller like this to enable loading of remote css directly from this controller [PluginController("UmbracoApi")] [ValidationFilter] @@ -124,54 +123,8 @@ namespace Umbraco.Web.Editors [ValidateAngularAntiForgeryToken] public IEnumerable> GetDashboard(string section) { - var tabs = new List>(); - var i = 1; - - // The dashboard config can contain more than one area inserted by a package. - foreach( var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section))) - { - //we need to validate access to this section - if (DashboardSecurity.AuthorizeAccess(dashboardSection, Security.CurrentUser, Services.SectionService) == false) - continue; - - //User is authorized - foreach (var tab in dashboardSection.Tabs) - { - //we need to validate access to this tab - if (DashboardSecurity.AuthorizeAccess(tab, Security.CurrentUser, Services.SectionService) == false) - continue; - - var dashboardControls = new List(); - - foreach (var control in tab.Controls) - { - if (DashboardSecurity.AuthorizeAccess(control, Security.CurrentUser, Services.SectionService) == false) - continue; - - var dashboardControl = new DashboardControl(); - var controlPath = control.ControlPath.Trim(); - dashboardControl.Path = IOHelper.FindFile(controlPath); - if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant())) - dashboardControl.ServerSide = true; - - dashboardControls.Add(dashboardControl); - } - - tabs.Add(new Tab - { - Id = i, - Alias = tab.Caption.ToSafeAlias(), - IsActive = i == 1, - Label = tab.Caption, - Properties = dashboardControls - }); - - i++; - } - } - - //In case there are no tabs or a user doesn't have access the empty tabs list is returned - return tabs; + var dashboardHelper = new DashboardHelper(Services.SectionService); + return dashboardHelper.GetDashboard(section, Security.CurrentUser); } } } diff --git a/src/Umbraco.Web/Editors/DashboardHelper.cs b/src/Umbraco.Web/Editors/DashboardHelper.cs new file mode 100644 index 0000000000..8b43335ebe --- /dev/null +++ b/src/Umbraco.Web/Editors/DashboardHelper.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors +{ + internal class DashboardHelper + { + private readonly ISectionService _sectionService; + + public DashboardHelper(ISectionService sectionService) + { + if (sectionService == null) throw new ArgumentNullException("sectionService"); + _sectionService = sectionService; + } + + /// + /// Returns the dashboard models per section for the current user and it's access + /// + /// + /// + public IDictionary>> GetDashboards(IUser currentUser) + { + var result = new Dictionary>>(); + foreach (var section in _sectionService.GetSections()) + { + result[section.Alias] = GetDashboard(section.Alias, currentUser); + } + return result; + } + + /// + /// Returns the dashboard model for the given section based on the current user and it's access + /// + /// + /// + /// + public IEnumerable> GetDashboard(string section, IUser currentUser) + { + var tabs = new List>(); + var i = 1; + + // The dashboard config can contain more than one area inserted by a package. + foreach (var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section))) + { + //we need to validate access to this section + if (DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService) == false) + continue; + + //User is authorized + foreach (var tab in dashboardSection.Tabs) + { + //we need to validate access to this tab + if (DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService) == false) + continue; + + var dashboardControls = new List(); + + foreach (var control in tab.Controls) + { + if (DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService) == false) + continue; + + var dashboardControl = new DashboardControl(); + var controlPath = control.ControlPath.Trim(); + dashboardControl.Path = IOHelper.FindFile(controlPath); + if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant())) + dashboardControl.ServerSide = true; + + dashboardControls.Add(dashboardControl); + } + + tabs.Add(new Tab + { + Id = i, + Alias = tab.Caption.ToSafeAlias(), + IsActive = i == 1, + Label = tab.Caption, + Properties = dashboardControls + }); + + i++; + } + } + + //In case there are no tabs or a user doesn't have access the empty tabs list is returned + return tabs; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/SectionController.cs b/src/Umbraco.Web/Editors/SectionController.cs index cdb5b29b7b..6563e18b17 100644 --- a/src/Umbraco.Web/Editors/SectionController.cs +++ b/src/Umbraco.Web/Editors/SectionController.cs @@ -5,6 +5,8 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using System.Linq; +using System.Net.Http.Formatting; +using Umbraco.Web.Trees; namespace Umbraco.Web.Editors { @@ -15,9 +17,45 @@ namespace Umbraco.Web.Editors public class SectionController : UmbracoAuthorizedJsonController { public IEnumerable
GetSections() - { - var sections = Services.SectionService.GetAllowedSections(UmbracoUser.Id); - return sections.Select(Mapper.Map); + { + + var sections = Services.SectionService.GetAllowedSections(Security.GetUserId()); + + var sectionModels = sections.Select(Mapper.Map).ToArray(); + + //Check if there are empty dashboards or dashboards that will end up empty based on the current user's access + //and add the meta data about them + var dashboardHelper = new DashboardHelper(Services.SectionService); + //this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that + //since tree's by nature are controllers and require request contextual data. + var appTreeController = new ApplicationTreeController + { + ControllerContext = ControllerContext + }; + var dashboards = dashboardHelper.GetDashboards(Security.CurrentUser); + //now we can add metadata for each section so that the UI knows if there's actually anything at all to render for + //a dashboard for a given section, then the UI can deal with it accordingly (i.e. redirect to the first tree) + foreach (var section in sectionModels) + { + var hasDashboards = false; + IEnumerable> dashboardsForSection; + if (dashboards.TryGetValue(section.Alias, out dashboardsForSection)) + { + if (dashboardsForSection.Any()) + hasDashboards = true; + } + + if (hasDashboards == false) + { + //get the first tree in the section and get it's root node route path + var sectionTrees = appTreeController.GetApplicationTrees(section.Alias, null, null).Result; + section.RoutePath = sectionTrees.IsContainer == false + ? sectionTrees.RoutePath + : sectionTrees.Children[0].RoutePath; + } + } + + return sectionModels; } public IEnumerable
GetAllSections() diff --git a/src/Umbraco.Web/Models/ContentEditing/Section.cs b/src/Umbraco.Web/Models/ContentEditing/Section.cs index b0c1839297..06ece10ddf 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Section.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Section.cs @@ -23,6 +23,13 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "alias")] public string Alias { get; set; } + + /// + /// In some cases a custom route path can be specified so that when clicking on a section it goes to this + /// path instead of the normal dashboard path + /// + [DataMember(Name = "routePath")] + public string RoutePath { get; set; } } } diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index c66cb1d22c..dcec993dfe 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.Trees var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture); //find all tree definitions that have the current application alias - var appTrees = ApplicationContext.Current.Services.ApplicationTreeService.GetApplicationTrees(application, onlyInitialized).ToArray(); + var appTrees = Services.ApplicationTreeService.GetApplicationTrees(application, onlyInitialized).ToArray(); if (string.IsNullOrEmpty(tree) == false || appTrees.Length == 1) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 14ba427bbb..9046c92f48 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -320,6 +320,7 @@ + From 9be275cf4061e3ca6292dc92b4d0413f97d41539 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 17:49:21 +1000 Subject: [PATCH 564/714] Updates the routes to also redirect if there isn't any dashboard to view for the user --- .../application/umbsections.directive.js | 4 +- .../src/common/services/section.service.js | 43 +++++++++++++++++++ src/Umbraco.Web.UI.Client/src/routes.js | 30 +++++++------ 3 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/section.service.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js index 6183151c2a..969622abdb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js @@ -3,7 +3,7 @@ * @name umbraco.directives.directive:umbSections * @restrict E **/ -function sectionsDirective($timeout, $window, navigationService, treeService, sectionResource, appState, eventsService, $location) { +function sectionsDirective($timeout, $window, navigationService, treeService, sectionService, appState, eventsService, $location) { return { restrict: "E", // restrict to an element replace: true, // replace the html element with the template @@ -31,7 +31,7 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se }; function loadSections(){ - sectionResource.getSections() + sectionService.getSectionsForUser() .then(function (result) { scope.sections = result; calculateHeight(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/section.service.js b/src/Umbraco.Web.UI.Client/src/common/services/section.service.js new file mode 100644 index 0000000000..e563a83722 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/section.service.js @@ -0,0 +1,43 @@ +/** + * @ngdoc service + * @name umbraco.services.sectionService + * + * + * @description + * A service to return the sections (applications) to be listed in the navigation which are contextual to the current user + */ +(function () { + 'use strict'; + + function sectionService(userService, $q, sectionResource) { + + function getSectionsForUser() { + var deferred = $q.defer(); + userService.getCurrentUser().then(function (u) { + //if they've already loaded, return them + if (u.sections) { + deferred.resolve(u.sections); + } + else { + sectionResource.getSections().then(function (sections) { + //set these to the user (cached), then the user changes, these will be wiped + u.sections = sections; + deferred.resolve(u.sections); + }); + } + }); + return deferred.promise; + } + + var service = { + getSectionsForUser: getSectionsForUser + }; + + return service; + + } + + angular.module('umbraco.services').factory('sectionService', sectionService); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 7eafd7106d..2fb5f9beee 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -101,22 +101,28 @@ app.config(function ($routeProvider) { //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. template: "
", //This controller will execute for this route, then we can execute some code in order to set the template Url - controller: function ($scope, $route, $routeParams, $location) { + controller: function ($scope, $route, $routeParams, $location, sectionService) { if ($routeParams.section.toLowerCase() === "default" || $routeParams.section.toLowerCase() === "umbraco" || $routeParams.section === "") { $routeParams.section = "content"; } - //TODO: Here we could run some extra logic to check if the dashboard we are navigating - //to has any content to show and if not it could redirect to the first tree root path. - //BUT! this would mean that we'd need a server side call to check this data.... - //Instead we already have this data in the sections returned from the sectionResource but we - //don't want to cache data in a resource so we'd have to create a sectionService which would rely - //on the userService, then we update the umbsections.directive to use the sectionService and when the - //sectionService requests the sections, it caches the result against the current user. Then we can - //use the sectionService here to do the redirection. - - $routeParams.url = "dashboard.aspx?app=" + $routeParams.section; - $scope.templateUrl = 'views/common/dashboard.html'; + //We are going to check the currently loaded sections for the user and if the section we are navigating + //to has a custom route path we'll use that + sectionService.getSectionsForUser().then(function(sections) { + //find the one we're requesting + var found = _.find(sections, function(s) { + return s.alias === $routeParams.section; + }) + if (found && found.routePath) { + //there's a custom route path so redirect + $location.path(found.routePath); + } + else { + //there's no custom route path so continue as normal + $routeParams.url = "dashboard.aspx?app=" + $routeParams.section; + $scope.templateUrl = 'views/common/dashboard.html'; + } + }); }, resolve: canRoute(true) }) From 97f2b40f4bb6652f94da167d9cb82892c37f980b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 17:59:17 +1000 Subject: [PATCH 565/714] fixes tests --- src/Umbraco.Web/Models/Mapping/SectionModelMapper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web/Models/Mapping/SectionModelMapper.cs b/src/Umbraco.Web/Models/Mapping/SectionModelMapper.cs index 4f6015967d..787eea5bc4 100644 --- a/src/Umbraco.Web/Models/Mapping/SectionModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/SectionModelMapper.cs @@ -11,6 +11,7 @@ namespace Umbraco.Web.Models.Mapping public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { config.CreateMap() + .ForMember(section => section.RoutePath, x => x.Ignore()) .ForMember( dto => dto.Name, expression => expression.MapFrom(section => ui.Text("sections", section.Alias))) From 12eb496939a539ccc711cf4846d436cc8ba99fdc Mon Sep 17 00:00:00 2001 From: mikkelhm Date: Thu, 10 Aug 2017 12:36:57 +0200 Subject: [PATCH 566/714] Adds the avatar column as a table to update in the 7.7.0 migration, and sets its length to 500 --- src/Umbraco.Core/Models/Rdbms/UserDto.cs | 1 + .../Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 75de20d626..1507b4adf8 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -101,6 +101,7 @@ namespace Umbraco.Core.Models.Rdbms ///
[Column("avatar")] [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] public string Avatar { get; set; } [ResultColumn] diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs index 3374e4a53f..b3fa51c0b3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs @@ -32,6 +32,9 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("invitedDate")) == false) Create.Column("invitedDate").OnTable("umbracoUser").AsDateTime().Nullable(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("avatar")) == false) + Create.Column("avatar").OnTable("umbracoUser").AsString(500).Nullable(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("passwordConfig")) == false) { Create.Column("passwordConfig").OnTable("umbracoUser").AsString(500).Nullable(); @@ -43,7 +46,6 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe Execute.Sql("UPDATE umbracoUser SET passwordConfig = '" + json + "'"); } } - } public override void Down() From d1109fb9adc673751195b18a38a1d0a0e68d87c2 Mon Sep 17 00:00:00 2001 From: Jeavon Date: Thu, 10 Aug 2017 11:41:19 +0100 Subject: [PATCH 567/714] Fix CustomErrorsCheck so that success message can contain "On" as well as "RemoteOnly" as it's also a success value --- src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs index e564563ac4..3a42dfbc03 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config get { return _textService.Localize("healthcheck/customErrorsCheckSuccessMessage", - new[] { Values.First(v => v.IsRecommended).Value }); + new[] { CurrentValue }); } } From 3104e6d11c43492c72dd63ccd6566a9153701fe2 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 10 Aug 2017 13:39:33 +0200 Subject: [PATCH 568/714] fixes: U4-10243 Umbraco 7.7. Difference between "Enable/Disable" and "LockedOut" --- .../src/common/resources/users.resource.js | 17 +++++++ .../src/views/users/user.controller.js | 37 +++++++++----- .../src/views/users/user.html | 48 ++++++++++++------- .../users/views/users/users.controller.js | 34 +++++++++++++ .../src/views/users/views/users/users.html | 11 +++++ .../umbraco/config/lang/en_us.xml | 5 ++ src/Umbraco.Web/Editors/UsersController.cs | 23 +++++++++ 7 files changed, 147 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 0acd30afe0..55a8636bdd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -55,6 +55,22 @@ 'Failed to enable the users ' + userIds.join(",")); } + function unlockUsers(userIds) { + if (!userIds) { + throw "userIds not specified"; + } + + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = "userIds=" + userIds.join("&userIds="); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostUnlockUsers", qry)), + 'Failed to enable the users ' + userIds.join(",")); + } + function setUserGroupsOnUsers(userGroups, userIds) { var userGroupAliases = userGroups.map(function(o) { return o.alias; }); var query = "userGroupAliases=" + userGroupAliases.join("&userGroupAliases=") + "&userIds=" + userIds.join("&userIds="); @@ -185,6 +201,7 @@ var resource = { disableUsers: disableUsers, enableUsers: enableUsers, + unlockUsers: unlockUsers, setUserGroupsOnUsers: setUserGroupsOnUsers, getPagedResults: getPagedResults, getUser: getUser, diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 237c44255c..21e83346ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -30,6 +30,7 @@ vm.removeSelectedItem = removeSelectedItem; vm.disableUser = disableUser; vm.enableUser = enableUser; + vm.unlockUser = unlockUser; vm.clearAvatar = clearAvatar; vm.save = save; vm.toggleChangePassword = toggleChangePassword; @@ -234,11 +235,11 @@ function disableUser() { vm.disableUserButtonState = "busy"; usersResource.disableUsers([vm.user.id]).then(function (data) { - vm.user.userState = 1; - setUserDisplayState(); - vm.disableUserButtonState = "success"; - formHelper.showNotifications(data); - }, function(error){ + vm.user.userState = 1; + setUserDisplayState(); + vm.disableUserButtonState = "success"; + formHelper.showNotifications(data); + }, function (error) { vm.disableUserButtonState = "error"; formHelper.showNotifications(error.data); }); @@ -247,16 +248,28 @@ function enableUser() { vm.enableUserButtonState = "busy"; usersResource.enableUsers([vm.user.id]).then(function (data) { - vm.user.userState = 0; - setUserDisplayState(); - vm.enableUserButtonState = "success"; - formHelper.showNotifications(data); - }, function(error){ - vm.disableUserButtonState = "error"; + vm.user.userState = 0; + setUserDisplayState(); + vm.enableUserButtonState = "success"; + formHelper.showNotifications(data); + }, function (error) { + vm.enableUserButtonState = "error"; + formHelper.showNotifications(error.data); + }); + } + + function unlockUser() { + vm.unlockUserButtonState = "busy"; + usersResource.unlockUsers([vm.user.id]).then(function (data) { + vm.user.userState = 0; + setUserDisplayState(); + vm.unlockUserButtonState = "success"; + formHelper.showNotifications(data); + }, function (error) { + vm.unlockUserButtonState = "error"; formHelper.showNotifications(error.data); }); } - function clearAvatar() { // get user diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 84e50a3360..7d38936a2f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -1,6 +1,8 @@
- + +
@@ -233,29 +235,43 @@
-
- - -
+ size="s"> + +
+ +
+ + +
+ +
+
@@ -267,7 +283,7 @@ label-key="general_changePassword" state="changePasswordButtonState" ng-if="vm.changePasswordModel.isChanging === false" - size="m"> + size="s"> diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 96db5e07b1..5a0b6e60b0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -28,6 +28,7 @@ vm.allowDisableUser = true; vm.allowEnableUser = true; + vm.allowUnlockUser = true; vm.allowSetUserGroup = true; vm.layouts = [ @@ -76,6 +77,7 @@ vm.clickUser = clickUser; vm.disableUsers = disableUsers; vm.enableUsers = enableUsers; + vm.unlockUsers = unlockUsers; vm.openBulkUserGroupPicker = openBulkUserGroupPicker; vm.openUserGroupPicker = openUserGroupPicker; vm.removeSelectedUserGroup = removeSelectedUserGroup; @@ -241,6 +243,28 @@ }); } + function unlockUsers() { + vm.unlockUserButtonState = "busy"; + usersResource.unlockUsers(vm.selection).then(function (data) { + // update userState + angular.forEach(vm.selection, function (userId) { + var user = getUserFromArrayById(userId, vm.users); + if (user) { + user.userState = 0; + } + }); + // show the correct badges + setUserDisplayState(vm.users); + // show notification + formHelper.showNotifications(data); + vm.unlockUserButtonState = "init"; + clearSelection(); + }, function (error) { + vm.unlockUserButtonState = "error"; + formHelper.showNotifications(error.data); + }); + } + function getUserFromArrayById(userId, users) { return _.find(users, function (u) { return u.id === userId }); } @@ -549,6 +573,7 @@ // reset all states vm.allowDisableUser = true; vm.allowEnableUser = true; + vm.allowUnlockUser = true; vm.allowSetUserGroup = true; var firstSelectedUserGroups; @@ -563,6 +588,7 @@ if (user.isCurrentUser) { vm.allowDisableUser = false; vm.allowEnableUser = false; + vm.allowUnlockUser = false; vm.allowSetUserGroup = false; return; } @@ -579,6 +605,14 @@ vm.allowEnableUser = false; } + if (user.userDisplayState && user.userDisplayState.key === "LockedOut") { + vm.allowEnableUser = false; + } + + if (user.userDisplayState && user.userDisplayState.key !== "LockedOut") { + vm.allowUnlockUser = false; + } + // store the user group aliases of the first selected user if (!firstSelectedUserGroups) { firstSelectedUserGroups = user.userGroups.map(function (ug) { return ug.alias; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index c938329926..3c5ef78c8a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -81,6 +81,17 @@ action="vm.enableUsers()">
+
+ + +
Translate Update Set permissions + Unlock Content @@ -1149,6 +1150,10 @@ To manage your website, simply open the Umbraco back office and start adding con User groups have been set Deleted %0% user groups %0% was deleted + Unlocked %0% users + An error occurred while unlocking the users + %0% is now unlocked + An error occurred while unlocking the user Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 236c669db4..51864e4323 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -557,6 +557,29 @@ namespace Umbraco.Web.Editors return Request.CreateNotificationSuccessResponse( Services.TextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name })); + } + + /// + /// Unlocks the users with the given user ids + /// + /// + public HttpResponseMessage PostUnlockUsers([FromUri]int[] userIds) + { + var users = Services.UserService.GetUsersById(userIds).ToArray(); + foreach (var u in users) + { + u.IsLockedOut = false; + } + Services.UserService.Save(users); + + if (users.Length > 1) + { + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/unlockUsersSuccess", new[] { userIds.Length.ToString() })); + } + + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] { users[0].Name })); } public HttpResponseMessage PostSetUserGroupsOnUsers([FromUri]string[] userGroupAliases, [FromUri]int[] userIds) From 979a0c9e2526488bd531eda6f3e2c7ec01c8758a Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 10 Aug 2017 13:40:56 +0200 Subject: [PATCH 569/714] remove capitalise from badges --- src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less index 6998afa556..522b7564c1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less @@ -9,7 +9,6 @@ display: inline-flex; align-items: center; justify-content: center; - text-transform: capitalize; } // Colors From 0db4993900b9bafe143d26b9971b88c5eec57878 Mon Sep 17 00:00:00 2001 From: mikkelhm Date: Thu, 10 Aug 2017 13:53:26 +0200 Subject: [PATCH 570/714] Bump version to beta003 --- build/UmbracoVersion.txt | 2 +- src/SolutionInfo.cs | 2 +- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index e8bfb02cf8..ae27482bf3 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,3 +1,3 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) 7.7.0 -beta002 \ No newline at end of file +beta003 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index d46dbdaf46..2256adc07d 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.7.0")] -[assembly: AssemblyInformationalVersion("7.7.0-beta002")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.7.0-beta003")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 155cf3f124..ac5375bfe9 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). ///
/// The version comment. - public static string CurrentComment { get { return "beta002"; } } + public static string CurrentComment { get { return "beta003"; } } // Get the version of the umbraco.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx From 90213ec90de6a3cac593a446b5dc5754a0dd5458 Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 10 Aug 2017 15:26:03 +0200 Subject: [PATCH 571/714] no need to check twice if we just dont send any hidden children to the render method. --- .../ListDescendantsFromCurrentPage.cshtml | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml index b39eaf0c61..8105cfd972 100755 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml @@ -1,6 +1,6 @@ +@inherits Umbraco.Web.Mvc.UmbracoTemplatePage @using Umbraco.Core.Models @using Umbraco.Web -@inherits Umbraco.Web.Mvc.UmbracoTemplatePage @* This snippet creates links for every single page (no matter how deep) below @@ -16,7 +16,7 @@ var naviLevel = selection[0].Level; @* Add in level for a CSS hook *@ -
    +
      @* Loop through the selection *@ @foreach (var item in selection) { @@ -24,10 +24,13 @@ @item.Name @* if this child page has any children, where the property umbracoNaviHide is not True *@ - @if (item.Children.Any(x => x.IsVisible())) - { - @* Call our helper to display the children *@ - @ChildPages(item.Children.ToArray()) + @{ + var children = item.Children.Where(x => x.IsVisible()).ToArray(); + if (children.Length > 0) + { + @* Call our helper to display the children *@ + @ChildPages(children) + } } } @@ -40,21 +43,24 @@ @* Ensure that we have a collection of pages *@ if (selection.Length > 0) { - @* Get the first page in pages and get the level *@ + @* Get the first page in pages and get the level *@ var naviLevel = selection[0].Level; - @* Add in level for a CSS hook *@ + @* Add in level for a CSS hook *@
        - @foreach (var item in selection.Where(x => x.IsVisible())) + @foreach (var item in selection) {
      • @item.Name - @* if the this page has any children, where the property umbracoNaviHide is not True *@ - @if (item.Children.Any(x => x.IsVisible())) - { + @* if the page has any children, where the property umbracoNaviHide is not True *@ + @{ + var children = item.Children.Where(x => x.IsVisible()).ToArray(); + if (children.Length > 0) + { @* Recurse and call our helper to display the children *@ - @ChildPages(item.Children.ToArray()) + @ChildPages(children) + } }
      • } From b607c9cd87330fc91eaf84155abca084ea82147d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 10 Aug 2017 16:14:11 +0200 Subject: [PATCH 572/714] add documentation to users.resource --- .../src/common/resources/users.resource.js | 211 +++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 55a8636bdd..72564398c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -4,13 +4,33 @@ * @function * * @description - * Used by the users section to get users and send requests to create, invite, delete, etc. users. + * Used by the users section to get users and send requests to create, invite, disable, etc. users. */ (function () { 'use strict'; function usersResource($http, umbRequestHelper, $q, umbDataFormatter) { + /** + * @ngdoc method + * @name umbraco.resources.usersResource#clearAvatar + * @methodOf umbraco.resources.usersResource + * + * @description + * Deletes the user avatar + * + * ##usage + *
        +          * usersResource.clearAvatar(1)
        +          *    .then(function() {
        +          *        alert("avatar is gone");
        +          *    });
        +          * 
        + * + * @param {Array} id id of user. + * @returns {Promise} resourcePromise object. + * + */ function clearAvatar(userId) { return umbRequestHelper.resourcePromise( @@ -22,6 +42,26 @@ 'Failed to clear the user avatar ' + userId); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#disableUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Disables a collection of users + * + * ##usage + *
        +          * usersResource.disableUsers([1, 2, 3, 4, 5])
        +          *    .then(function() {
        +          *        alert("users were disabled");
        +          *    });
        +          * 
        + * + * @param {Array} ids ids of users to disable. + * @returns {Promise} resourcePromise object. + * + */ function disableUsers(userIds) { if (!userIds) { throw "userIds not specified"; @@ -39,6 +79,26 @@ 'Failed to disable the users ' + userIds.join(",")); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#enableUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Enables a collection of users + * + * ##usage + *
        +          * usersResource.enableUsers([1, 2, 3, 4, 5])
        +          *    .then(function() {
        +          *        alert("users were enabled");
        +          *    });
        +          * 
        + * + * @param {Array} ids ids of users to enable. + * @returns {Promise} resourcePromise object. + * + */ function enableUsers(userIds) { if (!userIds) { throw "userIds not specified"; @@ -55,6 +115,26 @@ 'Failed to enable the users ' + userIds.join(",")); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#unlockUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Unlocks a collection of users + * + * ##usage + *
        +          * usersResource.unlockUsers([1, 2, 3, 4, 5])
        +          *    .then(function() {
        +          *        alert("users were unlocked");
        +          *    });
        +          * 
        + * + * @param {Array} ids ids of users to unlock. + * @returns {Promise} resourcePromise object. + * + */ function unlockUsers(userIds) { if (!userIds) { throw "userIds not specified"; @@ -71,6 +151,27 @@ 'Failed to enable the users ' + userIds.join(",")); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#setUserGroupsOnUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Overwrites the existing user groups on a collection of users + * + * ##usage + *
        +          * usersResource.setUserGroupsOnUsers(['admin', 'editor'], [1, 2, 3, 4, 5])
        +          *    .then(function() {
        +          *        alert("users were updated");
        +          *    });
        +          * 
        + * + * @param {Array} userGroupAliases aliases of user groups. + * @param {Array} ids ids of users to update. + * @returns {Promise} resourcePromise object. + * + */ function setUserGroupsOnUsers(userGroups, userIds) { var userGroupAliases = userGroups.map(function(o) { return o.alias; }); var query = "userGroupAliases=" + userGroupAliases.join("&userGroupAliases=") + "&userIds=" + userIds.join("&userIds="); @@ -83,6 +184,34 @@ 'Failed to set user groups ' + userGroupAliases.join(",") + ' on the users ' + userIds.join(",")); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#getPagedResults + * @methodOf umbraco.resources.usersResource + * + * @description + * Get users + * + * ##usage + *
        +          * usersResource.getPagedResults({pageSize: 10, pageNumber: 2})
        +          *    .then(function(data) {
        +          *        var users = data.items;
        +          *        alert('they are here!');
        +          *    });
        +          * 
        + * + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of users per page, default = 25 + * @param {Int} options.pageNumber if paging data, current page index, default = 1 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order users by, default: `Username` + * @param {Array} options.userGroups property to filter users by user group + * @param {Array} options.userStates property to filter users by user state + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ function getPagedResults(options) { var defaults = { pageSize: 25, @@ -135,6 +264,26 @@ 'Failed to retrieve users paged result'); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#getUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Gets a user + * + * ##usage + *
        +          * usersResource.getUser(1)
        +          *    .then(function(user) {
        +          *        alert("It's here");
        +          *    });
        +          * 
        + * + * @param {Array} id user id. + * @returns {Promise} resourcePromise object containing the user. + * + */ function getUser(userId) { return umbRequestHelper.resourcePromise( @@ -146,6 +295,26 @@ "Failed to retrieve data for user " + userId); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#createUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Creates a new user + * + * ##usage + *
        +          * usersResource.createUser(user)
        +          *    .then(function(newUser) {
        +          *        alert("It's here");
        +          *    });
        +          * 
        + * + * @param {Object} user user to create + * @returns {Promise} resourcePromise object containing the new user. + * + */ function createUser(user) { if (!user) { throw "user not specified"; @@ -163,6 +332,26 @@ "Failed to save user"); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#inviteUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Creates and sends an email invitation to a new user + * + * ##usage + *
        +          * usersResource.inviteUser(user)
        +          *    .then(function(newUser) {
        +          *        alert("It's here");
        +          *    });
        +          * 
        + * + * @param {Object} user user to invite + * @returns {Promise} resourcePromise object containing the new user. + * + */ function inviteUser(user) { if (!user) { throw "user not specified"; @@ -180,6 +369,26 @@ "Failed to invite user"); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#saveUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Saves a user + * + * ##usage + *
        +          * usersResource.saveUser(user)
        +          *    .then(function(updatedUser) {
        +          *        alert("It's here");
        +          *    });
        +          * 
        + * + * @param {Object} user object to save + * @returns {Promise} resourcePromise object containing the updated user. + * + */ function saveUser(user) { if (!user) { throw "user not specified"; From f426e3c50048b349d1a7bb1f2e223ebc216836e2 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 11 Aug 2017 12:14:10 +0200 Subject: [PATCH 573/714] U4-10280 Add markers to required fields in user section --- .../components/html/umbcontrolgroup.directive.js | 7 ++++--- .../src/views/components/html/umb-control-group.html | 3 +++ .../src/views/components/users/change-password.html | 6 +++--- src/Umbraco.Web.UI.Client/src/views/users/user.html | 6 +++--- .../src/views/users/views/users/users.html | 10 +++++----- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbcontrolgroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbcontrolgroup.directive.js index 94ab5537dc..c4382240c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbcontrolgroup.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbcontrolgroup.directive.js @@ -10,8 +10,9 @@ angular.module("umbraco.directives.html") label: "@label", description: "@", hideLabel: "@", - alias: "@", - labelFor: "@" + alias: "@", + labelFor: "@", + required: "@?" }, require: '?^form', transclude: true, @@ -21,7 +22,7 @@ angular.module("umbraco.directives.html") link: function (scope, element, attr, formCtrl) { scope.formValid = function () { - if (formCtrl && scope.labelFor) { + if (formCtrl && scope.labelFor) { //if a label-for has been set, use that for the validation return formCtrl[scope.labelFor].$valid; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html index 06cb444cd5..d1356066c2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html @@ -3,6 +3,9 @@
        diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index a4e99546fa..790f2d1df6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -22,7 +22,7 @@ - + - + - +
        - + - +
        - +
        - + Required - + Required - - + + - +