diff --git a/src/Umbraco.Core/Auditing/AuditEventHandler.cs b/src/Umbraco.Core/Auditing/AuditEventsComponent.cs similarity index 83% rename from src/Umbraco.Core/Auditing/AuditEventHandler.cs rename to src/Umbraco.Core/Auditing/AuditEventsComponent.cs index 9b38d5b59e..57457f9241 100644 --- a/src/Umbraco.Core/Auditing/AuditEventHandler.cs +++ b/src/Umbraco.Core/Auditing/AuditEventsComponent.cs @@ -3,19 +3,21 @@ using System.Linq; using System.Text; using System.Threading; using System.Web; +using Umbraco.Core.Components; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Auditing { - public sealed class AuditEventHandler : ApplicationEventHandler + public sealed class AuditEventsComponent : UmbracoComponentBase, IUmbracoCoreComponent { - private IAuditService _auditServiceInstance; - private IUserService _userServiceInstance; - private IEntityService _entityServiceInstance; + private IAuditService _auditService; + private IUserService _userService; + private IEntityService _entityService; private IUser CurrentPerformingUser { @@ -24,13 +26,13 @@ namespace Umbraco.Core.Auditing var identity = Thread.CurrentPrincipal?.GetUmbracoIdentity(); return identity == null ? new User { Id = 0, Name = "SYSTEM", Email = "" } - : _userServiceInstance.GetUserById(Convert.ToInt32(identity.Id)); + : _userService.GetUserById(Convert.ToInt32(identity.Id)); } } private IUser GetPerformingUser(int userId) { - var found = userId >= 0 ? _userServiceInstance.GetUserById(userId) : null; + var found = userId >= 0 ? _userService.GetUserById(userId) : null; return found ?? new User {Id = 0, Name = "SYSTEM", Email = ""}; } @@ -45,11 +47,11 @@ namespace Umbraco.Core.Auditing } } - protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + public void Initialize(IAuditService auditService, IUserService userService, IEntityService entityService) { - _auditServiceInstance = applicationContext.Services.AuditService; - _userServiceInstance = applicationContext.Services.UserService; - _entityServiceInstance = applicationContext.Services.EntityService; + _auditService = auditService; + _userService = userService; + _entityService = entityService; //BackOfficeUserManager.AccountLocked += ; //BackOfficeUserManager.AccountUnlocked += ; @@ -63,7 +65,7 @@ namespace Umbraco.Core.Auditing BackOfficeUserManager.PasswordReset += OnPasswordReset; //BackOfficeUserManager.ResetAccessFailedCount += ; - UserService.SavedUserGroup2 += OnSavedUserGroupWithUsers; + UserService.SavedUserGroup += OnSavedUserGroupWithUsers; UserService.SavedUser += OnSavedUser; UserService.DeletedUser += OnDeletedUser; @@ -94,7 +96,7 @@ namespace Umbraco.Core.Auditing foreach (var id in args.MemberIds) { members.TryGetValue(id, out var member); - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", "umbraco/member/roles/removed", $"roles modified, removed {roles}"); @@ -109,7 +111,7 @@ namespace Umbraco.Core.Auditing foreach (var id in args.MemberIds) { members.TryGetValue(id, out var member); - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", "umbraco/member/roles/assigned", $"roles modified, assigned {roles}"); @@ -121,7 +123,7 @@ namespace Umbraco.Core.Auditing var performingUser = CurrentPerformingUser; var member = exportedMemberEventArgs.Member; - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", "umbraco/member/exported", "exported member data"); @@ -134,7 +136,7 @@ namespace Umbraco.Core.Auditing { var group = groupWithUser.UserGroup; - var dp = string.Join(", ", ((UserGroup)group).GetPreviouslyDirtyProperties()); + var dp = string.Join(", ", ((UserGroup)group).GetWereDirtyProperties()); var sections = ((UserGroup)group).WasPropertyDirty("AllowedSections") ? string.Join(", ", group.AllowedSections) : null; @@ -153,7 +155,7 @@ namespace Umbraco.Core.Auditing sb.Append($"default perms: {perms}"); } - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, -1, $"User Group {group.Id} \"{group.Name}\" ({group.Alias})", "umbraco/user-group/save", $"{sb}"); @@ -162,7 +164,7 @@ namespace Umbraco.Core.Auditing foreach (var user in groupWithUser.RemovedUsers) { - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, user.Id, $"User \"{user.Name}\" {FormatEmail(user)}", "umbraco/user-group/save", $"Removed user \"{user.Name}\" {FormatEmail(user)} from group {group.Id} \"{group.Name}\" ({group.Alias})"); @@ -170,7 +172,7 @@ namespace Umbraco.Core.Auditing foreach (var user in groupWithUser.AddedUsers) { - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, user.Id, $"User \"{user.Name}\" {FormatEmail(user)}", "umbraco/user-group/save", $"Added user \"{user.Name}\" {FormatEmail(user)} to group {group.Id} \"{group.Name}\" ({group.Alias})"); @@ -186,9 +188,9 @@ namespace Umbraco.Core.Auditing { var group = sender.GetUserGroupById(perm.UserGroupId); var assigned = string.Join(", ", perm.AssignedPermissions); - var entity = _entityServiceInstance.Get(perm.EntityId); + var entity = _entityService.Get(perm.EntityId); - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, -1, $"User Group {group.Id} \"{group.Name}\" ({group.Alias})", "umbraco/user-group/permissions-change", $"assigning {(string.IsNullOrWhiteSpace(assigned) ? "(nothing)" : assigned)} on id:{perm.EntityId} \"{entity.Name}\""); @@ -201,9 +203,9 @@ namespace Umbraco.Core.Auditing var members = saveEventArgs.SavedEntities; foreach (var member in members) { - var dp = string.Join(", ", ((Member) member).GetPreviouslyDirtyProperties()); + var dp = string.Join(", ", ((Member) member).GetWereDirtyProperties()); - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", "umbraco/member/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); @@ -216,7 +218,7 @@ namespace Umbraco.Core.Auditing var members = deleteEventArgs.DeletedEntities; foreach (var member in members) { - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", "umbraco/member/delete", $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}"); @@ -233,9 +235,9 @@ namespace Umbraco.Core.Auditing ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) : null; - var dp = string.Join(", ", ((User)affectedUser).GetPreviouslyDirtyProperties()); + var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties()); - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", "umbraco/user/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? "" : "; groups assigned: " + groups)}"); @@ -247,7 +249,7 @@ namespace Umbraco.Core.Auditing var performingUser = CurrentPerformingUser; var affectedUsers = deleteEventArgs.DeletedEntities; foreach (var affectedUser in affectedUsers) - _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", "umbraco/user/delete", "delete user"); @@ -313,7 +315,7 @@ namespace Umbraco.Core.Auditing private void WriteAudit(int performingId, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null) { - var performingUser = _userServiceInstance.GetUserById(performingId); + var performingUser = _userService.GetUserById(performingId); var performingDetails = performingUser == null ? $"User UNKNOWN:{performingId}" @@ -335,13 +337,13 @@ namespace Umbraco.Core.Auditing { if (affectedDetails == null) { - var affectedUser = _userServiceInstance.GetUserById(affectedId); + var affectedUser = _userService.GetUserById(affectedId); affectedDetails = affectedUser == null ? $"User UNKNOWN:{affectedId}" : $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}"; } - _auditServiceInstance.Write(performingId, performingDetails, + _auditService.Write(performingId, performingDetails, ipAddress, DateTime.UtcNow, affectedId, affectedDetails, diff --git a/src/Umbraco.Core/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs index 4853f89560..48d35efde9 100644 --- a/src/Umbraco.Core/Collections/DeepCloneableList.cs +++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs @@ -136,5 +136,11 @@ namespace Umbraco.Core.Collections dc.ResetDirtyProperties(rememberDirty); } } + + /// Always return an empty enumerable, the list has no properties that can be dirty. + public IEnumerable GetWereDirtyProperties() + { + return Enumerable.Empty(); + } } } diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 0ed7289b30..33b8443d11 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -483,7 +483,7 @@ namespace Umbraco.Core.Configuration { get { - var setting = ConfigurationManager.AppSettings.ContainsKey("umbracoLocalTempStorage"); + var setting = ConfigurationManager.AppSettings["umbracoLocalTempStorage"]; if (!string.IsNullOrWhiteSpace(setting)) return Enum.Parse(setting); diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index d401eaee88..4c859469fd 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -126,8 +126,6 @@ public const string Languages = "languages"; - public const string Macros = "macros"; - /// /// alias for the user types tree. /// diff --git a/src/Umbraco.Core/Models/Entities/EntitySlim.cs b/src/Umbraco.Core/Models/Entities/EntitySlim.cs index fbef1fafbd..163879bbe0 100644 --- a/src/Umbraco.Core/Models/Entities/EntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/EntitySlim.cs @@ -215,6 +215,11 @@ namespace Umbraco.Core.Models.Entities throw new WontImplementException(); } + public IEnumerable GetWereDirtyProperties() + { + throw new WontImplementException(); + } + #endregion } } diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index eab9e21013..bd4905729d 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -390,6 +390,10 @@ namespace Umbraco.Core.Models.Identity _beingDirty.ResetDirtyProperties(rememberDirty); } + /// + public IEnumerable GetWereDirtyProperties() + => _beingDirty.GetWereDirtyProperties(); + /// /// Disables change tracking. /// diff --git a/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs new file mode 100644 index 0000000000..b49cdf4ae1 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Represents the configuration for the color picker value editor. + /// + public class ColorPickerConfiguration : ValueListConfiguration + { + [ConfigurationField("useLabel", "Include labels?", "boolean", Description = "Stores colors as a Json object containing both the color hex string and label, rather than just the hex string.")] + public bool UseLabel { get; set; } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index 6ae55d94cb..60912edad0 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using System.Xml.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Composing; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -179,6 +180,9 @@ namespace Umbraco.Core.PropertyEditors /// internal Attempt TryConvertValueToCrlType(object value) { + if (value is JValue) + value = value.ToString(); + //this is a custom check to avoid any errors, if it's a string and it's empty just make it null if (value is string s && string.IsNullOrWhiteSpace(s)) value = null; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs index 9e0ec37f5b..9f260fc973 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs @@ -1,4 +1,6 @@ using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters @@ -10,15 +12,50 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPicker); public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (string); + => UseLabel(propertyType) ? typeof(PickedColor) : typeof(string); public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) => PropertyCacheLevel.Element; public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) { - // make sure it's a string - return source?.ToString() ?? string.Empty; + var useLabel = UseLabel(propertyType); + + if (source == null) return useLabel ? null : string.Empty; + + var ssource = source.ToString(); + if (ssource.DetectIsJson()) + { + try + { + var jo = JsonConvert.DeserializeObject(ssource); + if (useLabel) return new PickedColor(jo["value"].ToString(), jo["label"].ToString()); + return jo["value"].ToString(); + } + catch { /* not json finally */ } + } + + if (useLabel) return new PickedColor(ssource, ssource); + return ssource; + } + + private bool UseLabel(PublishedPropertyType propertyType) + { + return ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration).UseLabel; + } + + public class PickedColor + { + public PickedColor(string color, string label) + { + Color = color; + Label = label; + } + + public string Color { get; } + public string Label { get; } + + public override string ToString() => Color; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 5efd40778b..3117fc0b54 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -51,8 +51,8 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters var gridConfig = UmbracoConfig.For.GridConfig( Current.ProfilingLogger.Logger, Current.ApplicationCache.RuntimeCache, - new DirectoryInfo(HttpContext.Current.Server.MapPath(SystemDirectories.AppPlugins)), - new DirectoryInfo(HttpContext.Current.Server.MapPath(SystemDirectories.Config)), + new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)), + new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config)), HttpContext.Current.IsDebuggingEnabled); var sections = GetArray(obj, "sections"); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index db199eed05..b91383715a 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -18,6 +18,8 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) => PropertyCacheLevel.Element; + private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; + public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) { // data is (both in database and xml): @@ -52,7 +54,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // fall back on normal behaviour return values.Any() == false - ? sourceString.Split(new string[] { Environment.NewLine }, StringSplitOptions.None) + ? sourceString.Split(NewLineDelimiters, StringSplitOptions.None) : values.ToArray(); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs similarity index 85% rename from src/Umbraco.Web/PropertyEditors/ValueListConfiguration.cs rename to src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs index 30d2aa77cd..e40cdab646 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace Umbraco.Web.PropertyEditors +namespace Umbraco.Core.PropertyEditors { /// /// Represents the ValueList editor configuration. /// - class ValueListConfiguration + public class ValueListConfiguration { [JsonProperty("items")] public List Items { get; set; } = new List(); @@ -20,4 +20,4 @@ namespace Umbraco.Web.PropertyEditors public string Value { get; set; } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index eff8b1f958..17bb32675b 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -324,7 +324,7 @@ namespace Umbraco.Core.Security Guid guidSession; if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out guidSession)) { - ApplicationContext.Current.Services.UserService.ClearLoginSession(guidSession); + Current.Services.UserService.ClearLoginSession(guidSession); } } } diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 4df824544c..cec1ee6bcb 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -1,30 +1,16 @@ -using System; -using System.Linq; +using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using System.Web; using Microsoft.AspNet.Identity; using Umbraco.Core.Models.Identity; -using Umbraco.Core.Services; namespace Umbraco.Core.Security { public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory where T: BackOfficeIdentityUser { - private readonly ApplicationContext _appCtx; - - [Obsolete("Use the overload specifying all dependencies instead")] public BackOfficeClaimsIdentityFactory() - :this(ApplicationContext.Current) { - } - - public BackOfficeClaimsIdentityFactory(ApplicationContext appCtx) - { - if (appCtx == null) throw new ArgumentNullException("appCtx"); - _appCtx = appCtx; - SecurityStampClaimType = Constants.Security.SessionIdClaimType; UserNameClaimType = ClaimTypes.Name; } @@ -58,14 +44,5 @@ namespace Umbraco.Core.Security } public class BackOfficeClaimsIdentityFactory : BackOfficeClaimsIdentityFactory - { - [Obsolete("Use the overload specifying all dependencies instead")] - public BackOfficeClaimsIdentityFactory() - { - } - - public BackOfficeClaimsIdentityFactory(ApplicationContext appCtx) : base(appCtx) - { - } - } + { } } diff --git a/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs index 58543f106c..686cefcdf8 100644 --- a/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs +++ b/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs @@ -1,52 +1,37 @@ using System; using System.Collections.Concurrent; -using System.ComponentModel; using System.Globalization; -using System.Net.Http.Headers; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; -using Semver; using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Identity; +using Umbraco.Core.Services; namespace Umbraco.Core.Security { public class BackOfficeCookieAuthenticationProvider : CookieAuthenticationProvider { - private readonly ApplicationContext _appCtx; + private readonly IUserService _userService; + private readonly IRuntimeState _runtimeState; - [Obsolete("Use the ctor specifying all dependencies")] - [EditorBrowsable(EditorBrowsableState.Never)] - public BackOfficeCookieAuthenticationProvider() - : this(ApplicationContext.Current) + public BackOfficeCookieAuthenticationProvider(IUserService userService, IRuntimeState runtimeState) { + _userService = userService; + _runtimeState = runtimeState; } - public BackOfficeCookieAuthenticationProvider(ApplicationContext appCtx) - { - if (appCtx == null) throw new ArgumentNullException("appCtx"); - _appCtx = appCtx; - } - - private static readonly SemVersion MinUmbracoVersionSupportingLoginSessions = new SemVersion(7, 8); - public override void ResponseSignIn(CookieResponseSignInContext context) { - var backOfficeIdentity = context.Identity as UmbracoBackOfficeIdentity; - if (backOfficeIdentity != null) + if (context.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) { //generate a session id and assign it //create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one - //NOTE - special check because when we are upgrading to 7.8 we cannot create a session since the db isn't ready and we'll get exceptions - var canAcquireSession = _appCtx.IsUpgrading == false || _appCtx.CurrentVersion() >= MinUmbracoVersionSupportingLoginSessions; - - var session = canAcquireSession - ? _appCtx.Services.UserService.CreateLoginSession((int)backOfficeIdentity.Id, context.OwinContext.GetCurrentRequestIpAddress()) + var session = _runtimeState.Level == RuntimeLevel.Run + ? _userService.CreateLoginSession((int)backOfficeIdentity.Id, context.OwinContext.GetCurrentRequestIpAddress()) : Guid.NewGuid(); backOfficeIdentity.UserData.SessionId = session.ToString(); @@ -58,15 +43,13 @@ namespace Umbraco.Core.Security public override void ResponseSignOut(CookieResponseSignOutContext context) { //Clear the user's session on sign out - if (context != null && context.OwinContext != null && context.OwinContext.Authentication != null - && context.OwinContext.Authentication.User != null && context.OwinContext.Authentication.User.Identity != null) + if (context?.OwinContext?.Authentication?.User?.Identity != null) { var claimsIdentity = context.OwinContext.Authentication.User.Identity as ClaimsIdentity; var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType); - Guid guidSession; - if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out guidSession)) + if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out var guidSession)) { - _appCtx.Services.UserService.ClearLoginSession(guidSession); + _userService.ClearLoginSession(guidSession); } } @@ -117,7 +100,7 @@ namespace Umbraco.Core.Security /// protected virtual async Task EnsureValidSessionId(CookieValidateIdentityContext context) { - if (_appCtx.IsConfigured && _appCtx.IsUpgrading == false) + if (_runtimeState.Level == RuntimeLevel.Run) await SessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context); } diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index f39c06524d..72838c3d55 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -54,6 +54,8 @@ namespace Umbraco.Core.Services xml.Add(new XAttribute("template", content.Template?.Id.ToString(CultureInfo.InvariantCulture) ?? "0")); + xml.Add(new XAttribute("isPublished", content.Published)); + if (withDescendants) { var descendants = contentService.GetDescendants(content).ToArray(); diff --git a/src/Umbraco.Core/Services/IAuditService.cs b/src/Umbraco.Core/Services/IAuditService.cs index 19ad180df3..13d84f802e 100644 --- a/src/Umbraco.Core/Services/IAuditService.cs +++ b/src/Umbraco.Core/Services/IAuditService.cs @@ -1,15 +1,88 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { + /// + /// Represents a service for handling audit. + /// public interface IAuditService : IService { void Add(AuditType type, string comment, int userId, int objectId); - IEnumerable GetLogs(int objectId); - IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null); - IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null); + + IEnumerable GetLogs(int objectId); + IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null); + IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null); void CleanLogs(int maximumAgeOfLogsInMinutes); + + /// + /// Returns paged items in the audit trail for a given entity + /// + /// + /// + /// + /// + /// + /// By default this will always be ordered descending (newest first) + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter + /// so we need to do that here + /// + /// + /// Optional filter to be applied + /// + /// + IEnumerable GetPagedItemsByEntity(int entityId, long pageIndex, int pageSize, out long totalRecords, + Direction orderDirection = Direction.Descending, + AuditType[] auditTypeFilter = null, + IQuery customFilter = null); + + /// + /// Returns paged items in the audit trail for a given user + /// + /// + /// + /// + /// + /// + /// By default this will always be ordered descending (newest first) + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter + /// so we need to do that here + /// + /// + /// Optional filter to be applied + /// + /// + IEnumerable GetPagedItemsByUser(int userId, long pageIndex, int pageSize, out long totalRecords, + Direction orderDirection = Direction.Descending, + AuditType[] auditTypeFilter = null, + IQuery customFilter = null); + + /// + /// Writes an audit entry for an audited event. + /// + /// The identifier of the user triggering the audited event. + /// Free-form details about the user triggering the audited event. + /// The IP address or the request triggering the audited event. + /// The date and time of the audited event. + /// The identifier of the user affected by the audited event. + /// Free-form details about the entity affected by the audited event. + /// + /// The type of the audited event - must contain only alphanumeric chars and hyphens with forward slashes separating categories. + /// + /// The eventType will generally be formatted like: {application}/{entity-type}/{category}/{sub-category} + /// Example: umbraco/user/sign-in/failed + /// + /// + /// Free-form details about the audited event. + IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string affectedDetails, string eventType, string eventDetails); + } } diff --git a/src/Umbraco.Core/Services/IConsentService.cs b/src/Umbraco.Core/Services/IConsentService.cs new file mode 100644 index 0000000000..fdcf18bc74 --- /dev/null +++ b/src/Umbraco.Core/Services/IConsentService.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + /// + /// A service for handling lawful data processing requirements + /// + /// + /// Consent can be given or revoked or changed via the method, which + /// creates a new entity to track the consent. Revoking a consent is performed by + /// registering a revoked consent. + /// A consent can be revoked, by registering a revoked consent, but cannot be deleted. + /// Getter methods return the current state of a consent, i.e. the latest + /// entity that was created. + /// + public interface IConsentService : IService + { + /// + /// Registers consent. + /// + /// The source, i.e. whoever is consenting. + /// + /// + /// The state of the consent. + /// Additional free text. + /// The corresponding consent entity. + IConsent RegisterConsent(string source, string context, string action, ConsentState state, string comment = null); + + /// + /// Retrieves consents. + /// + /// The optional source. + /// The optional context. + /// The optional action. + /// Determines whether is a start pattern. + /// Determines whether is a start pattern. + /// Determines whether is a start pattern. + /// Determines whether to include the history of consents. + /// Consents matching the paramters. + IEnumerable LookupConsent(string source = null, string context = null, string action = null, + bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false, + bool includeHistory = false); + } +} diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index ebc063fbe7..b08a7ed55d 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -43,7 +43,17 @@ namespace Umbraco.Core.Services /// Creates a new content item from a blueprint. /// IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0); - + + /// + /// Deletes blueprints for a content type. + /// + void DeleteBlueprintsOfType(int contentTypeId, int userId = 0); + + /// + /// Deletes blueprints for content types. + /// + void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = 0); + #endregion #region Get, Count Documents @@ -326,6 +336,11 @@ namespace Umbraco.Core.Services /// bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); + /// + /// Sorts documents. + /// + bool Sort(IEnumerable ids, int userId = 0, bool raiseEvents = true); + #endregion #region Publish Document diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index a9394126f8..ffe87451c3 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Net.Http; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 8c22cffff7..543931196f 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -12,6 +13,34 @@ namespace Umbraco.Core.Services /// public interface IUserService : IMembershipUserService { + /// + /// Creates a database entry for starting a new login session for a user + /// + /// + /// + /// + Guid CreateLoginSession(int userId, string requestingIpAddress); + + /// + /// Validates that a user login session is valid/current and hasn't been closed + /// + /// + /// + /// + bool ValidateLoginSession(int userId, Guid sessionId); + + /// + /// Removes the session's validity + /// + /// + void ClearLoginSession(Guid sessionId); + + /// + /// Removes all valid sessions for the user + /// + /// + int ClearLoginSessions(int userId); + /// /// This is basically facets of UserStates key = state, value = count /// diff --git a/src/Umbraco.Core/Services/IdkMap.cs b/src/Umbraco.Core/Services/IdkMap.cs index a6a7cf1965..0332e88399 100644 --- a/src/Umbraco.Core/Services/IdkMap.cs +++ b/src/Umbraco.Core/Services/IdkMap.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Threading; using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Scoping; namespace Umbraco.Core.Services @@ -37,8 +39,13 @@ namespace Umbraco.Core.Services int? val; using (var scope = _scopeProvider.CreateScope()) { - val = scope.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id AND nodeObjectType=@nodeObjectType", - new { id = key, nodeObjectType = GetNodeObjectTypeGuid(umbracoObjectType) }); + var sql = scope.Database.SqlContext.Sql() + .Select(x => x.NodeId).From().Where(x => x.UniqueId == key); + + if (umbracoObjectType != UmbracoObjectTypes.Unknown) // if unknow, don't include in query + sql = sql.Where(x => x.NodeObjectType == GetNodeObjectTypeGuid(umbracoObjectType) || x.NodeObjectType == Constants.ObjectTypes.IdReservation); // fixme TEST the OR here! + + val = scope.Database.ExecuteScalar(sql); scope.Complete(); } @@ -89,8 +96,13 @@ namespace Umbraco.Core.Services Guid? val; using (var scope = _scopeProvider.CreateScope()) { - val = scope.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id AND nodeObjectType=@nodeObjectType", - new { id, nodeObjectType = GetNodeObjectTypeGuid(umbracoObjectType) }); + var sql = scope.Database.SqlContext.Sql() + .Select(x => x.UniqueId).From().Where(x => x.NodeId == id); + + if (umbracoObjectType != UmbracoObjectTypes.Unknown) // if unknow, don't include in query + sql = sql.Where(x => x.NodeObjectType == GetNodeObjectTypeGuid(umbracoObjectType) || x.NodeObjectType == Constants.ObjectTypes.IdReservation); // fixme TEST the OR here! + + val = scope.Database.ExecuteScalar(sql); scope.Complete(); } @@ -104,7 +116,7 @@ namespace Umbraco.Core.Services { _locker.EnterWriteLock(); _id2Key[id] = new TypedId(val.Value, umbracoObjectType); - _key2Id[val.Value] = new TypedId(); + _key2Id[val.Value] = new TypedId(id, umbracoObjectType); } finally { diff --git a/src/Umbraco.Core/Services/Implement/AuditService.cs b/src/Umbraco.Core/Services/Implement/AuditService.cs index 0e04121bd6..bcd19a72ac 100644 --- a/src/Umbraco.Core/Services/Implement/AuditService.cs +++ b/src/Umbraco.Core/Services/Implement/AuditService.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; @@ -10,13 +14,17 @@ namespace Umbraco.Core.Services.Implement { public sealed class AuditService : ScopeRepositoryService, IAuditService { + private readonly Lazy _isAvailable; private readonly IAuditRepository _auditRepository; + private readonly IAuditEntryRepository _auditEntryRepository; public AuditService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IAuditRepository auditRepository) + IAuditRepository auditRepository, IAuditEntryRepository auditEntryRepository) : base(provider, logger, eventMessagesFactory) { _auditRepository = auditRepository; + _auditEntryRepository = auditEntryRepository; + _isAvailable = new Lazy(DetermineIsAvailable); } public void Add(AuditType type, string comment, int userId, int objectId) @@ -28,35 +36,35 @@ namespace Umbraco.Core.Services.Implement } } - public IEnumerable GetLogs(int objectId) + public IEnumerable GetLogs(int objectId) { using (var scope = ScopeProvider.CreateScope()) { - var result = _auditRepository.Get(Query().Where(x => x.Id == objectId)); + var result = _auditRepository.Get(Query().Where(x => x.Id == objectId)); scope.Complete(); return result; } } - public IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null) + public IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null) { using (var scope = ScopeProvider.CreateScope()) { var result = sinceDate.HasValue == false - ? _auditRepository.Get(Query().Where(x => x.UserId == userId && x.AuditType == type)) - : _auditRepository.Get(Query().Where(x => x.UserId == userId && x.AuditType == type && x.CreateDate >= sinceDate.Value)); + ? _auditRepository.Get(Query().Where(x => x.UserId == userId && x.AuditType == type)) + : _auditRepository.Get(Query().Where(x => x.UserId == userId && x.AuditType == type && x.CreateDate >= sinceDate.Value)); scope.Complete(); return result; } } - public IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null) + public IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null) { using (var scope = ScopeProvider.CreateScope()) { var result = sinceDate.HasValue == false - ? _auditRepository.Get(Query().Where(x => x.AuditType == type)) - : _auditRepository.Get(Query().Where(x => x.AuditType == type && x.CreateDate >= sinceDate.Value)); + ? _auditRepository.Get(Query().Where(x => x.AuditType == type)) + : _auditRepository.Get(Query().Where(x => x.AuditType == type && x.CreateDate >= sinceDate.Value)); scope.Complete(); return result; } @@ -70,5 +78,163 @@ namespace Umbraco.Core.Services.Implement scope.Complete(); } } + + /// + /// Returns paged items in the audit trail for a given entity + /// + /// + /// + /// + /// + /// + /// By default this will always be ordered descending (newest first) + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter + /// so we need to do that here + /// + /// + /// Optional filter to be applied + /// + /// + public IEnumerable GetPagedItemsByEntity(int entityId, long pageIndex, int pageSize, out long totalRecords, + Direction orderDirection = Direction.Descending, + AuditType[] auditTypeFilter = null, + IQuery customFilter = null) + { + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + if (entityId == Constants.System.Root || entityId <= 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + + using (ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.Id == entityId); + + return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter); + } + } + + /// + /// Returns paged items in the audit trail for a given user + /// + /// + /// + /// + /// + /// + /// By default this will always be ordered descending (newest first) + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter + /// so we need to do that here + /// + /// + /// Optional filter to be applied + /// + /// + public IEnumerable GetPagedItemsByUser(int userId, long pageIndex, int pageSize, out long totalRecords, Direction orderDirection = Direction.Descending, AuditType[] auditTypeFilter = null, IQuery customFilter = null) + { + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + if (userId < 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + + using (ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.UserId == userId); + + return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter); + } + } + + /// + public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string affectedDetails, string eventType, string eventDetails) + { + if (performingUserId < 0) throw new ArgumentOutOfRangeException(nameof(performingUserId)); + if (string.IsNullOrWhiteSpace(perfomingDetails)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(perfomingDetails)); + if (string.IsNullOrWhiteSpace(eventType)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventType)); + if (string.IsNullOrWhiteSpace(eventDetails)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventDetails)); + + //we need to truncate the data else we'll get SQL errors + affectedDetails = affectedDetails?.Substring(0, Math.Min(affectedDetails.Length, AuditEntryDto.DetailsLength)); + eventDetails = eventDetails.Substring(0, Math.Min(eventDetails.Length, AuditEntryDto.DetailsLength)); + + //validate the eventType - must contain a forward slash, no spaces, no special chars + var eventTypeParts = eventType.ToCharArray(); + if (eventTypeParts.Contains('/') == false || eventTypeParts.All(c => char.IsLetterOrDigit(c) || c == '/' || c == '-') == false) + throw new ArgumentException(nameof(eventType) + " must contain only alphanumeric characters, hyphens and at least one '/' defining a category"); + if (eventType.Length > AuditEntryDto.EventTypeLength) + throw new ArgumentException($"Must be max {AuditEntryDto.EventTypeLength} chars.", nameof(eventType)); + if (performingIp != null && performingIp.Length > AuditEntryDto.IpLength) + throw new ArgumentException($"Must be max {AuditEntryDto.EventTypeLength} chars.", nameof(performingIp)); + + var entry = new AuditEntry + { + PerformingUserId = performingUserId, + PerformingDetails = perfomingDetails, + PerformingIp = performingIp, + EventDateUtc = eventDateUtc, + AffectedUserId = affectedUserId, + AffectedDetails = affectedDetails, + EventType = eventType, + EventDetails = eventDetails + }; + + if (_isAvailable.Value == false) return entry; + + using (var scope = ScopeProvider.CreateScope()) + { + _auditEntryRepository.Save(entry); + scope.Complete(); + } + + return entry; + } + + //TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead + internal IEnumerable GetAll() + { + if (_isAvailable.Value == false) return Enumerable.Empty(); + + using (ScopeProvider.CreateScope(autoComplete: true)) + { + return _auditEntryRepository.GetMany(); + } + } + + //TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead + internal IEnumerable GetPage(long pageIndex, int pageCount, out long records) + { + if (_isAvailable.Value == false) + { + records = 0; + return Enumerable.Empty(); + } + + using (ScopeProvider.CreateScope(autoComplete: true)) + { + return _auditEntryRepository.GetPage(pageIndex, pageCount, out records); + } + } + + /// + /// Determines whether the repository is available. + /// + private bool DetermineIsAvailable() + { + using (ScopeProvider.CreateScope(autoComplete: true)) + { + return _auditEntryRepository.IsAvailable(); + } + } } } diff --git a/src/Umbraco.Core/Services/Implement/ConsentService.cs b/src/Umbraco.Core/Services/Implement/ConsentService.cs new file mode 100644 index 0000000000..21ec5f4434 --- /dev/null +++ b/src/Umbraco.Core/Services/Implement/ConsentService.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Services.Implement +{ + /// + /// Implements . + /// + internal class ConsentService : ScopeRepositoryService, IConsentService + { + private readonly IConsentRepository _consentRepository; + + /// + /// Initializes a new instance of the class. + /// + public ConsentService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IConsentRepository consentRepository) + : base(provider, logger, eventMessagesFactory) + { + _consentRepository = consentRepository; + } + + /// + public IConsent RegisterConsent(string source, string context, string action, ConsentState state, string comment = null) + { + // prevent stupid states + var v = 0; + if ((state & ConsentState.Pending) > 0) v++; + if ((state & ConsentState.Granted) > 0) v++; + if ((state & ConsentState.Revoked) > 0) v++; + if (v != 1) + throw new ArgumentException("Invalid state.", nameof(state)); + + var consent = new Consent + { + Current = true, + Source = source, + Context = context, + Action = action, + CreateDate = DateTime.Now, + State = state, + Comment = comment + }; + + using (var scope = ScopeProvider.CreateScope()) + { + _consentRepository.ClearCurrent(source, context, action); + _consentRepository.Save(consent); + scope.Complete(); + } + + return consent; + } + + /// + public IEnumerable LookupConsent(string source = null, string context = null, string action = null, + bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false, + bool includeHistory = false) + { + using (ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query(); + + if (string.IsNullOrWhiteSpace(source) == false) + query = sourceStartsWith ? query.Where(x => x.Source.StartsWith(source)) : query.Where(x => x.Source == source); + if (string.IsNullOrWhiteSpace(context) == false) + query = contextStartsWith ? query.Where(x => x.Context.StartsWith(context)) : query.Where(x => x.Context == context); + if (string.IsNullOrWhiteSpace(action) == false) + query = actionStartsWith ? query.Where(x => x.Action.StartsWith(action)) : query.Where(x => x.Action == action); + if (includeHistory == false) + query = query.Where(x => x.Current); + + return _consentRepository.Get(query); + } + } + } +} diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 0deb396978..789ae30cab 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -27,7 +27,7 @@ namespace Umbraco.Core.Services.Implement private readonly IContentTypeRepository _contentTypeRepository; private readonly IDocumentBlueprintRepository _documentBlueprintRepository; - private readonly MediaFileSystem _mediaFileSystem; + private readonly MediaFileSystem _mediaFileSystem; private IQuery _queryNotTrashed; #region Constructors @@ -63,7 +63,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.CountPublished(); + return _documentRepository.CountPublished(contentTypeAlias); } } @@ -651,7 +651,7 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - query.Where(x => x.Path.SqlStartsWith($"{contentPath[0]},", TextColumnType.NVarchar)); + query.Where(x => x.Path.SqlStartsWith($"{contentPath[0].Path},", TextColumnType.NVarchar)); } return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } @@ -800,8 +800,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var bin = $"{Constants.System.Root},{Constants.System.RecycleBinContent},"; - var query = Query().Where(x => x.Path.StartsWith(bin)); + var query = Query().Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix)); return _documentRepository.Get(query); } } @@ -1709,7 +1708,7 @@ namespace Umbraco.Core.Services.Implement /// /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . + /// to the ordering of items in the passed in . /// /// /// Using this method will ensure that the Published-state is maintained upon sorting @@ -1726,56 +1725,88 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(itemsA); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - return false; - - var published = new List(); - var saved = new List(); - scope.WriteLock(Constants.Locks.ContentTree); - var sortOrder = 0; - - foreach (var content in itemsA) - { - // if the current sort order equals that of the content we don't - // need to update it, so just increment the sort order and continue. - if (content.SortOrder == sortOrder) - { - sortOrder++; - continue; - } - - // else update - content.SortOrder = sortOrder++; - content.WriterId = userId; - - // if it's published, register it, no point running StrategyPublish - // since we're not really publishing it and it cannot be cancelled etc - if (content.Published) - published.Add(content); - - // save - saved.Add(content); - _documentRepository.Save(content); - } - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); - - if (raiseEvents && published.Any()) - scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); - - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); + var ret = Sort(scope, itemsA, userId, raiseEvents); scope.Complete(); + return ret; + } + } + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items identified by the . + /// + /// + /// Using this method will ensure that the Published-state is maintained upon sorting + /// so the cache is updated accordingly - as needed. + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + public bool Sort(IEnumerable ids, int userId = 0, bool raiseEvents = true) + { + var idsA = ids.ToArray(); + if (idsA.Length == 0) return true; + + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + var itemsA = GetByIds(idsA).ToArray(); + + var ret = Sort(scope, itemsA, userId, raiseEvents); + scope.Complete(); + return ret; + } + } + + private bool Sort(IScope scope, IContent[] itemsA, int userId, bool raiseEvents) + { + var saveEventArgs = new SaveEventArgs(itemsA); + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + return false; + + var published = new List(); + var saved = new List(); + var sortOrder = 0; + + foreach (var content in itemsA) + { + // if the current sort order equals that of the content we don't + // need to update it, so just increment the sort order and continue. + if (content.SortOrder == sortOrder) + { + sortOrder++; + continue; + } + + // else update + content.SortOrder = sortOrder++; + content.WriterId = userId; + + // if it's published, register it, no point running StrategyPublish + // since we're not really publishing it and it cannot be cancelled etc + if (content.Published) + published.Add(content); + + // save + saved.Add(content); + _documentRepository.Save(content); } + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); + + if (raiseEvents && published.Any()) + scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); + + Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); return true; } @@ -2302,6 +2333,38 @@ namespace Umbraco.Core.Services.Implement } } + public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = 0) + { + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + var contentTypeIdsA = contentTypeIds.ToArray(); + var query = Query(); + if (contentTypeIdsA.Length > 0) + query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId)); + + var blueprints = _documentBlueprintRepository.Get(query).Select(x => + { + ((Content) x).Blueprint = true; + return x; + }).ToArray(); + + foreach (var blueprint in blueprints) + { + _documentBlueprintRepository.Delete(blueprint); + } + + scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(blueprints), "DeletedBlueprint"); + scope.Complete(); + } + } + + public void DeleteBlueprintsOfType(int contentTypeId, int userId = 0) + { + DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId); + } + #endregion } } diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeService.cs index cbbe9e6f63..f6498770ec 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -32,8 +33,13 @@ namespace Umbraco.Core.Services.Implement protected override void DeleteItemsOfTypes(IEnumerable typeIds) { - foreach (var typeId in typeIds) - ContentService.DeleteOfType(typeId); + using (var scope = ScopeProvider.CreateScope()) + { + var typeIdsA = typeIds.ToArray(); + ContentService.DeleteOfTypes(typeIdsA); + ContentService.DeleteBlueprintsOfTypes(typeIdsA); + scope.Complete(); + } } /// @@ -82,6 +88,5 @@ namespace Umbraco.Core.Services.Implement return Repository.GetAllContentTypeIds(aliases); } } - } } diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs index 63c5340a6c..a3db23e5ed 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs @@ -130,7 +130,7 @@ namespace Umbraco.Core.Services.Implement protected void OnDeletedContainer(IScope scope, DeleteEventArgs args) { - scope.Events.Dispatch(DeletedContainer, This, args); + scope.Events.Dispatch(DeletedContainer, This, args, "DeletedContainer"); } } } diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 78c3032df2..dec4e56714 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -445,7 +445,7 @@ namespace Umbraco.Core.Services.Implement //null check otherwise we get exceptions if (media.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - var rootId = Constants.System.Root.ToInvariantString(); + var rootId = Constants.System.RootString; var ids = media.Path.Split(',') .Where(x => x != rootId && x != media.Id.ToString(CultureInfo.InvariantCulture)) .Select(int.Parse) @@ -616,7 +616,7 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - query.Where(x => x.Path.SqlStartsWith(mediaPath[0] + ",", TextColumnType.NVarchar)); + query.Where(x => x.Path.SqlStartsWith(mediaPath[0].Path + ",", TextColumnType.NVarchar)); } return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } @@ -706,8 +706,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); - var bin = $"{Constants.System.Root},{Constants.System.RecycleBinMedia},"; - var query = Query().Where(x => x.Path.StartsWith(bin)); + var query = Query().Where(x => x.Path.StartsWith(Constants.System.RecycleBinMediaPathPrefix)); return _mediaRepository.Get(query); } } @@ -734,7 +733,7 @@ namespace Umbraco.Core.Services.Implement /// public IMedia GetMediaByPath(string mediaPath) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { return _mediaRepository.GetMediaByPath(mediaPath); } diff --git a/src/Umbraco.Core/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/Implement/MemberService.cs index 83ec78932f..f8245f18c2 100644 --- a/src/Umbraco.Core/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberService.cs @@ -403,7 +403,7 @@ namespace Umbraco.Core.Services.Implement { scope.ReadLock(Constants.Locks.MemberTree); var query1 = memberTypeAlias == null ? null : Query().Where(x => x.ContentTypeAlias == memberTypeAlias); - var query2 = filter == null ? null : Query().Where(x => x.Name.Contains(filter) || x.Username.Contains(filter)); + var query2 = filter == null ? null : Query().Where(x => x.Name.Contains(filter) || x.Username.Contains(filter) || x.Email.Contains(filter)); return _memberRepository.GetPage(query1, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, query2); } } @@ -815,6 +815,10 @@ namespace Umbraco.Core.Services.Implement /// Default is True otherwise set to False to not raise events public void Save(IMember member, bool raiseEvents = true) { + //trimming username and email to make sure we have no trailing space + member.Username = member.Username.Trim(); + member.Email = member.Email.Trim(); + using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(member); @@ -866,7 +870,13 @@ namespace Umbraco.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); foreach (var member in membersA) + { + //trimming username and email to make sure we have no trailing space + member.Username = member.Username.Trim(); + member.Email = member.Email.Trim(); + _memberRepository.Save(member); + } if (raiseEvents) { @@ -1018,7 +1028,9 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); - _memberGroupRepository.AssignRoles(usernames, roleNames); + var ids = _memberGroupRepository.GetMemberIds(usernames); + _memberGroupRepository.AssignRoles(ids, roleNames); + scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames)); scope.Complete(); } } @@ -1033,7 +1045,9 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); - _memberGroupRepository.DissociateRoles(usernames, roleNames); + var ids = _memberGroupRepository.GetMemberIds(usernames); + _memberGroupRepository.DissociateRoles(ids, roleNames); + scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames)); scope.Complete(); } } @@ -1049,6 +1063,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.AssignRoles(memberIds, roleNames); + scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(memberIds, roleNames)); scope.Complete(); } } @@ -1064,6 +1079,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.DissociateRoles(memberIds, roleNames); + scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(memberIds, roleNames)); scope.Complete(); } } @@ -1110,6 +1126,21 @@ namespace Umbraco.Core.Services.Implement /// public static event TypedEventHandler> Saved; + /// + /// Occurs after roles have been assigned. + /// + public static event TypedEventHandler AssignedRoles; + + /// + /// Occurs after roles have been removed. + /// + public static event TypedEventHandler RemovedRoles; + + /// + /// Occurs after members have been exported. + /// + internal static event TypedEventHandler Exported; + #endregion #region Membership @@ -1219,6 +1250,72 @@ namespace Umbraco.Core.Services.Implement return member; } + /// + /// Exports a member. + /// + /// + /// This is internal for now and is used to export a member in the member editor, + /// it will raise an event so that auditing logs can be created. + /// + internal MemberExportModel ExportMember(Guid key) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.Key == key); + var member = _memberRepository.Get(query).FirstOrDefault(); + + if (member == null) return null; + + var model = new MemberExportModel + { + Id = member.Id, + Key = member.Key, + Name = member.Name, + Username = member.Username, + Email = member.Email, + Groups = GetAllRoles(member.Id).ToList(), + ContentTypeAlias = member.ContentTypeAlias, + CreateDate = member.CreateDate, + UpdateDate = member.UpdateDate, + Properties = new List(GetPropertyExportItems(member)) + }; + + scope.Events.Dispatch(Exported, this, new ExportedMemberEventArgs(member, model)); + + return model; + } + } + + private static IEnumerable GetPropertyExportItems(IMember member) + { + if (member == null) throw new ArgumentNullException(nameof(member)); + + var exportProperties = new List(); + + foreach (var property in member.Properties) + { + //ignore list + switch (property.Alias) + { + case Constants.Conventions.Member.PasswordQuestion: + continue; + } + + var propertyExportModel = new MemberExportProperty + { + Id = property.Id, + Alias = property.Alias, + Name = property.PropertyType.Name, + Value = property.GetValue(), // fixme ignoring variants + CreateDate = property.CreateDate, + UpdateDate = property.UpdateDate + }; + exportProperties.Add(propertyExportModel); + } + + return exportProperties; + } + #endregion #region Content Types diff --git a/src/Umbraco.Core/Services/Implement/PackagingService.cs b/src/Umbraco.Core/Services/Implement/PackagingService.cs index ea698fc8c9..b6ddc400d8 100644 --- a/src/Umbraco.Core/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Core/Services/Implement/PackagingService.cs @@ -768,21 +768,18 @@ namespace Umbraco.Core.Services.Implement foreach (var element in structureElement.Elements("DocumentType")) { var alias = element.Value; - if (_importedContentTypes.ContainsKey(alias)) - { - var allowedChild = _importedContentTypes[alias]; - if (allowedChild == null || allowedChildren.Any(x => x.Id.IsValueCreated && x.Id.Value == allowedChild.Id)) continue; - allowedChildren.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), sortOrder, allowedChild.Alias)); - sortOrder++; - } - else + var allowedChild = _importedContentTypes.ContainsKey(alias) ? _importedContentTypes[alias] : _contentTypeService.Get(alias); + if (allowedChild == null) { - _logger.Warn( - string.Format( - "Packager: Error handling DocumentType structure. DocumentType with alias '{0}' could not be found and was not added to the structure for '{1}'.", - alias, contentType.Alias)); + _logger.Warn($"Packager: Error handling DocumentType structure. DocumentType with alias '{alias}' could not be found and was not added to the structure for '{contentType.Alias}'."); + continue; } + + if (allowedChildren.Any(x => x.Id.IsValueCreated && x.Id.Value == allowedChild.Id)) continue; + + allowedChildren.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), sortOrder, allowedChild.Alias)); + sortOrder++; } contentType.AllowedContentTypes = allowedChildren; @@ -1679,9 +1676,10 @@ namespace Umbraco.Core.Services.Implement internal InstallationSummary InstallPackage(string packageFilePath, int userId = 0, bool raiseEvents = false) { + var metaData = GetPackageMetaData(packageFilePath); + if (raiseEvents) { - var metaData = GetPackageMetaData(packageFilePath); if (ImportingPackage.IsRaisedEventCancelled(new ImportPackageEventArgs(packageFilePath, metaData), this)) { var initEmpty = new InstallationSummary().InitEmpty(); @@ -1693,7 +1691,7 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents) { - ImportedPackage.RaiseEvent(new ImportPackageEventArgs(installationSummary, false), this); + ImportedPackage.RaiseEvent(new ImportPackageEventArgs(installationSummary, metaData, false), this); } return installationSummary; diff --git a/src/Umbraco.Core/Services/Implement/UserService.cs b/src/Umbraco.Core/Services/Implement/UserService.cs index 598f742e60..43d438da0b 100644 --- a/src/Umbraco.Core/Services/Implement/UserService.cs +++ b/src/Umbraco.Core/Services/Implement/UserService.cs @@ -224,9 +224,9 @@ namespace Umbraco.Core.Services.Implement } /// - /// Deletes an + /// Disables an /// - /// to Delete + /// to disable public void Delete(IUser membershipUser) { //disable @@ -542,6 +542,42 @@ namespace Umbraco.Core.Services.Implement } } + public Guid CreateLoginSession(int userId, string requestingIpAddress) + { + using (var scope = ScopeProvider.CreateScope()) + { + var session = _userRepository.CreateLoginSession(userId, requestingIpAddress); + scope.Complete(); + return session; + } + } + + public int ClearLoginSessions(int userId) + { + using (var scope = ScopeProvider.CreateScope()) + { + var count = _userRepository.ClearLoginSessions(userId); + scope.Complete(); + return count; + } + } + + public void ClearLoginSession(Guid sessionId) + { + using (var scope = ScopeProvider.CreateScope()) + { + _userRepository.ClearLoginSession(sessionId); + scope.Complete(); + } + } + + public bool ValidateLoginSession(int userId, Guid sessionId) + { + using (ScopeProvider.CreateScope(autoComplete: true)) + { + return _userRepository.ValidateLoginSession(userId, sessionId); + } + } public IDictionary GetUserStates() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -747,8 +783,9 @@ namespace Umbraco.Core.Services.Implement _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); scope.Complete(); - scope.Events.Dispatch(UserGroupPermissionsAssigned, this, new SaveEventArgs(entityIds.Select(x => new EntityPermission(groupId, x, permissions.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray())) - .ToArray(), false)); + var assigned = permissions.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); + scope.Events.Dispatch(UserGroupPermissionsAssigned, this, + new SaveEventArgs(entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(), false)); } } @@ -768,8 +805,9 @@ namespace Umbraco.Core.Services.Implement _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); scope.Complete(); - scope.Events.Dispatch(UserGroupPermissionsAssigned, this, new SaveEventArgs(entityIds.Select(x => new EntityPermission(groupId, x, new[] { permission.ToString(CultureInfo.InvariantCulture) })) - .ToArray(), false)); + var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; + scope.Events.Dispatch(UserGroupPermissionsAssigned, this, + new SaveEventArgs(entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(), false)); } } @@ -842,7 +880,23 @@ namespace Umbraco.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(userGroup); + // we need to figure out which users have been added / removed, for audit purposes + var empty = new IUser[0]; + var addedUsers = empty; + var removedUsers = empty; + + if (userIds != null) + { + var groupUsers = userGroup.HasIdentity ? _userRepository.GetAllInGroup(userGroup.Id).ToArray() : empty; + var xGroupUsers = groupUsers.ToDictionary(x => x.Id, x => x); + var groupIds = groupUsers.Select(x => x.Id).ToArray(); + + addedUsers = _userRepository.GetMany(userIds.Except(groupIds).ToArray()).Where(x => x.Id != 0).ToArray(); + removedUsers = groupIds.Except(userIds).Select(x => xGroupUsers[x]).Where(x => x.Id != 0).ToArray(); + } + + var saveEventArgs = new SaveEventArgs(new UserGroupWithUsers(userGroup, addedUsers, removedUsers)); + if (raiseEvents && scope.Events.DispatchCancelable(SavingUserGroup, this, saveEventArgs)) { scope.Complete(); @@ -1183,12 +1237,12 @@ namespace Umbraco.Core.Services.Implement /// /// Occurs before Save /// - public static event TypedEventHandler> SavingUserGroup; + internal static event TypedEventHandler> SavingUserGroup; /// /// Occurs after Save /// - public static event TypedEventHandler> SavedUserGroup; + internal static event TypedEventHandler> SavedUserGroup; /// /// Occurs before Delete diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index c8bdc7b2fd..ffc3bdfd31 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -34,12 +34,13 @@ namespace Umbraco.Core.Services private readonly Lazy _notificationService; private readonly Lazy _externalLoginService; private readonly Lazy _redirectUrlService; + private readonly Lazy _consentService; /// /// Initializes a new instance of the class with lazy services. /// /// Used by IoC. Note that LightInject will favor lazy args when picking a constructor. - public ServiceContext(Lazy publicAccessService, Lazy taskService, Lazy domainService, Lazy auditService, Lazy localizedTextService, Lazy tagService, Lazy contentService, Lazy userService, Lazy memberService, Lazy mediaService, Lazy contentTypeService, Lazy mediaTypeService, Lazy dataTypeService, Lazy fileService, Lazy localizationService, Lazy packagingService, Lazy serverRegistrationService, Lazy entityService, Lazy relationService, Lazy treeService, Lazy sectionService, Lazy macroService, Lazy memberTypeService, Lazy memberGroupService, Lazy notificationService, Lazy externalLoginService, Lazy redirectUrlService) + public ServiceContext(Lazy publicAccessService, Lazy taskService, Lazy domainService, Lazy auditService, Lazy localizedTextService, Lazy tagService, Lazy contentService, Lazy userService, Lazy memberService, Lazy mediaService, Lazy contentTypeService, Lazy mediaTypeService, Lazy dataTypeService, Lazy fileService, Lazy localizationService, Lazy packagingService, Lazy serverRegistrationService, Lazy entityService, Lazy relationService, Lazy treeService, Lazy sectionService, Lazy macroService, Lazy memberTypeService, Lazy memberGroupService, Lazy notificationService, Lazy externalLoginService, Lazy redirectUrlService, Lazy consentService) { _publicAccessService = publicAccessService; _taskService = taskService; @@ -68,14 +69,14 @@ namespace Umbraco.Core.Services _notificationService = notificationService; _externalLoginService = externalLoginService; _redirectUrlService = redirectUrlService; + _consentService = consentService; } /// /// Initializes a new instance of the class with services. /// /// Used in tests. All items are optional and remain null if not specified. - public ServiceContext( - IContentService contentService = null, + public ServiceContext(IContentService contentService = null, IMediaService mediaService = null, IContentTypeService contentTypeService = null, IMediaTypeService mediaTypeService = null, @@ -101,7 +102,8 @@ namespace Umbraco.Core.Services IPublicAccessService publicAccessService = null, IExternalLoginService externalLoginService = null, IServerRegistrationService serverRegistrationService = null, - IRedirectUrlService redirectUrlService = null) + IRedirectUrlService redirectUrlService = null, + IConsentService consentService = null) { if (serverRegistrationService != null) _serverRegistrationService = new Lazy(() => serverRegistrationService); if (externalLoginService != null) _externalLoginService = new Lazy(() => externalLoginService); @@ -130,6 +132,7 @@ namespace Umbraco.Core.Services if (macroService != null) _macroService = new Lazy(() => macroService); if (publicAccessService != null) _publicAccessService = new Lazy(() => publicAccessService); if (redirectUrlService != null) _redirectUrlService = new Lazy(() => redirectUrlService); + if (consentService != null) _consentService = new Lazy(() => consentService); } /// @@ -256,12 +259,20 @@ namespace Umbraco.Core.Services /// Gets the MemberGroupService /// public IMemberGroupService MemberGroupService => _memberGroupService.Value; - + + /// + /// Gets the ExternalLoginService. + /// public IExternalLoginService ExternalLoginService => _externalLoginService.Value; /// /// Gets the RedirectUrlService. /// public IRedirectUrlService RedirectUrlService => _redirectUrlService.Value; + + /// + /// Gets the ConsentService. + /// + public IConsentService ConsentService => _consentService.Value; } } diff --git a/src/Umbraco.Core/UdiGetterExtensions.cs b/src/Umbraco.Core/UdiGetterExtensions.cs index e94f228e4c..3ba5fe6f65 100644 --- a/src/Umbraco.Core/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/UdiGetterExtensions.cs @@ -132,7 +132,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IContent entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(entity.Blueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, entity.Key).EnsureClosed(); + return new GuidUdi(entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document, entity.Key).EnsureClosed(); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 9719a5f223..6a19b00f22 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -98,7 +98,7 @@ - + @@ -377,6 +377,7 @@ + @@ -395,6 +396,7 @@ + @@ -1349,6 +1351,7 @@ + @@ -1357,6 +1360,7 @@ + diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index d58814590d..88180428c4 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -152,7 +153,7 @@ namespace Umbraco.Core } catch (ArgumentException ex) { - LogHelper.Error(typeof(UriExtensions), "Failed to determine if request was client side", ex); + Current.Logger.Error(typeof(UriExtensions), "Failed to determine if request was client side", ex); return false; } } diff --git a/src/Umbraco.Web/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorPickerConfigurationEditor.cs index 5dee154638..d0cafaabc4 100644 --- a/src/Umbraco.Web/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.RegularExpressions; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.PropertyEditors; @@ -9,12 +10,11 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - internal class ColorPickerConfigurationEditor : ValueListConfigurationEditor + internal class ColorPickerConfigurationEditor : ConfigurationEditor { - public ColorPickerConfigurationEditor(ILocalizedTextService textService) - : base(textService) + public ColorPickerConfigurationEditor() { - var field = Fields.First(); + var field = Fields.First(x => x.Key == "items"); //use a custom editor too field.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; @@ -26,23 +26,84 @@ namespace Umbraco.Web.PropertyEditors field.Validators.Add(new ColorListValidator()); } - public override Dictionary ToConfigurationEditor(ValueListConfiguration configuration) + public override Dictionary ToConfigurationEditor(ColorPickerConfiguration configuration) { if (configuration == null) return new Dictionary { - { "items", new object() } + { "items", new object() }, + { "useLabel", false } }; - // for now, we have to do this, because the color picker is weird, but it's fixed in 7.7 at some point - // and then we probably don't need this whole override method anymore - base shouls be enough? + var items = configuration.Items.ToDictionary(x => x.Id.ToString(), x => GetItemValue(x, configuration.UseLabel)); return new Dictionary { - { "items", configuration.Items.ToDictionary(x => x.Id.ToString(), x => x.Value) } + { "items", items }, + { "useLabel", configuration.UseLabel } }; } + private object GetItemValue(ValueListConfiguration.ValueListItem item, bool useLabel) + { + if (useLabel) + { + return item.Value.DetectIsJson() + ? JsonConvert.DeserializeObject(item.Value) + : new JObject { { "color", item.Value }, { "label", item.Value } }; + } + + if (!item.Value.DetectIsJson()) + return item.Value; + + var jobject = (JObject) JsonConvert.DeserializeObject(item.Value); + return jobject.Property("color").Value.Value(); + } + + public override ColorPickerConfiguration FromConfigurationEditor(Dictionary editorValues, ColorPickerConfiguration configuration) + { + var output = new ColorPickerConfiguration(); + + if (!editorValues.TryGetValue("items", out var jjj) || !(jjj is JArray jItems)) + return output; // oops + + // handle useLabel + if (editorValues.TryGetValue("useLabel", out var useLabelObj)) + output.UseLabel = useLabelObj.TryConvertTo(); + + // auto-assigning our ids, get next id from existing values + var nextId = 1; + if (configuration?.Items != null && configuration.Items.Count > 0) + nextId = configuration.Items.Max(x => x.Id) + 1; + + // create ValueListItem instances - sortOrder is ignored here + foreach (var item in jItems.OfType()) + { + var value = item.Property("value")?.Value?.Value(); + if (string.IsNullOrWhiteSpace(value)) continue; + + var id = item.Property("id")?.Value?.Value() ?? 0; + if (id >= nextId) nextId = id + 1; + + // if using a label, replace color by json blob + // (a pity we have to serialize here!) + if (output.UseLabel) + { + var label = item.Property("label")?.Value?.Value(); + value = JsonConvert.SerializeObject(new { value, label }); + } + + output.Items.Add(new ValueListConfiguration.ValueListItem { Id = id, Value = value }); + } + + // ensure ids + foreach (var item in output.Items) + if (item.Id == 0) + item.Id = nextId++; + + return output; + } + internal class ColorListValidator : IValueValidator { public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 20358b3d4d..88f9430f2b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -298,7 +298,6 @@ -