From e8fd7e23734b9852e8f6e63e661bdc299c93ef8d Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 26 Feb 2018 15:02:47 +1100 Subject: [PATCH] Fixes for U4-10900 Individual member data export functionality --- src/Umbraco.Core/Services/IMemberService.cs | 10 +- src/Umbraco.Core/Services/MemberService.cs | 102 -------------- .../Plugins/PluginManagerTests.cs | 2 +- src/Umbraco.Web/Editors/MemberController.cs | 131 +++++++++++++++++- src/Umbraco.Web/Trees/MemberTreeController.cs | 9 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 + .../Umbraco.Web.csproj.DotSettings | 2 +- src/umbraco.cms/Actions/ActionExportMember.cs | 86 ------------ src/umbraco.cms/umbraco.cms.csproj | 1 - 9 files changed, 140 insertions(+), 205 deletions(-) delete mode 100644 src/umbraco.cms/Actions/ActionExportMember.cs diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index d57e110a16..021b0c4fbe 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -199,15 +199,7 @@ namespace Umbraco.Core.Services /// /// Id of the MemberType void DeleteMembersOfType(int memberTypeId); - - /// - /// Exports member data based on their unique Id - /// - /// The unique member identifier - /// The user requesting the export - /// - HttpResponseMessage ExportMemberData(Guid key, IUser currentUser); - + [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 71751c4d4e..33563e5285 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -258,108 +258,6 @@ namespace Umbraco.Core.Services } } - /// - /// Exports member data based on their unique Id - /// - /// The unique member identifier - /// The user requesting the export - /// - public HttpResponseMessage ExportMemberData(Guid key, IUser currentUser) - { - var httpResponseMessage = new HttpResponseMessage(); - if (currentUser.HasAccessToSensitiveData() == false) - { - httpResponseMessage.StatusCode = HttpStatusCode.Forbidden; - return httpResponseMessage; - } - - var memberPropertyFilter = new List - { - "RawPasswordValue", - "ParentId", - "SortOrder", - "Level", - "Path", - "CreatorId", - "Version", - "ContentTypeId", - "HasIdentity", - "PropertyGroups", - "PropertyTypes", - "ProviderUserKey", - "ContentType" - }; - var propertiesFilter = new List - { - "PropertyType", - "Version", - "Id", - "HasIdentity", - "Key" - }; - - var member = GetByKey(key); - var memberProperties = member.GetType().GetProperties(); - var fileName = $"{member.Name}_{member.Email}.txt"; - - using (var memoryStream = new MemoryStream()) - { - using (var textWriter = new StreamWriter(memoryStream)) - { - foreach (var memberProperty in memberProperties) - { - if (memberPropertyFilter.Contains(memberProperty.Name)) - continue; - - var propertyValue = memberProperty.GetValue(member, null); - var type = propertyValue?.GetType(); - - if (type == typeof(PropertyCollection)) - { - textWriter.WriteLine(""); - textWriter.WriteLine("PROPERTIES"); - textWriter.WriteLine("**********"); - - if (propertyValue is PropertyCollection propertyCollection) - { - foreach (var property in propertyCollection) - { - var propProperties = property.GetType().GetProperties(); - - textWriter.WriteLine("Name : " + property.PropertyType.Name); - - foreach (var p in propProperties) - { - if (propertiesFilter.Contains(p.Name)) continue; - var pValue = p.GetValue(property, null); - textWriter.WriteLine(p.Name + " : " + pValue); - } - - textWriter.WriteLine("------------------------"); - } - } - } - else - { - textWriter.WriteLine(memberProperty.Name + " : " + propertyValue); - } - } - - textWriter.Flush(); - } - - httpResponseMessage.Content = new ByteArrayContent(memoryStream.ToArray()); - httpResponseMessage.Content.Headers.Add("x-filename", fileName); - httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); - httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName; - httpResponseMessage.StatusCode = HttpStatusCode.OK; - - return httpResponseMessage; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 0cd03df990..22d150ac3b 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -293,7 +293,7 @@ AnotherContentFinder public void Resolves_Actions() { var actions = _manager.ResolveActions(); - Assert.AreEqual(39, actions.Count()); + Assert.AreEqual(38, actions.Count()); } [Test] diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 24035db814..96233592ac 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Web; @@ -783,8 +786,132 @@ namespace Umbraco.Web.Editors [HttpGet] public HttpResponseMessage ExportMemberData(Guid key) { - var currentUser = UmbracoContext.Current.Security.CurrentUser; - return Services.MemberService.ExportMemberData(key, currentUser); + var currentUser = Security.CurrentUser; + + var httpResponseMessage = Request.CreateResponse(); + if (currentUser.HasAccessToSensitiveData() == false) + { + httpResponseMessage.StatusCode = HttpStatusCode.Forbidden; + return httpResponseMessage; + } + + var member = Services.MemberService.GetByKey(key); + var memberProperties = member.GetType().GetProperties().ToList(); + //since we want to write the property types last, we'll re-order this list + var propertyTypesProperties = memberProperties.Where(x => x.PropertyType == typeof(PropertyCollection)).ToList(); + foreach (var propertyType in propertyTypesProperties) + { + memberProperties.Remove(propertyType); + } + //now re-add them to the end (there will only be one, but we'll do this just to be complete) + foreach (var propertyTypesProperty in propertyTypesProperties) + { + memberProperties.Add(propertyTypesProperty); + } + + var fileName = $"{member.Name}_{member.Email}.txt"; + + using (var memoryStream = new MemoryStream()) + { + using (var textWriter = new StreamWriter(memoryStream)) + { + foreach (var memberProperty in memberProperties) + { + ReportWriter.WritePropertyValue(member, memberProperty, textWriter); + } + + textWriter.Flush(); + } + + httpResponseMessage.Content = new ByteArrayContent(memoryStream.ToArray()); + httpResponseMessage.Content.Headers.Add("x-filename", fileName); + httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); + httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName; + httpResponseMessage.StatusCode = HttpStatusCode.OK; + + return httpResponseMessage; + } + } + + private static class ReportWriter + { + private static readonly List MemberPropertyFilter = new List + { + "RawPasswordValue", + "RawPasswordAnswerValue", + "ParentId", + "SortOrder", + "Level", + "Path", + "CreatorId", + "Version", + "ContentTypeId", + "HasIdentity", + "PropertyGroups", + "PropertyTypes", + "ProviderUserKey", + "ContentType", + "DeletedDate" + }; + private static readonly List PropertiesFilter = new List + { + "PropertyType", + "Version", + "Id", + "HasIdentity", + "Key", + "DeletedDate" + }; + + public static void WritePropertyValue(object owner, PropertyInfo prop, StreamWriter textWriter) + { + if (owner == null) throw new ArgumentNullException(nameof(owner)); + if (prop == null) throw new ArgumentNullException(nameof(prop)); + if (textWriter == null) throw new ArgumentNullException(nameof(textWriter)); + if (MemberPropertyFilter.Contains(prop.Name)) + return; + + var propertyValue = prop.GetValue(owner, null) ?? string.Empty; + var type = prop.PropertyType; + + if (propertyValue is DateTime time) + { + textWriter.WriteLine(prop.Name + " : " + time.ToIsoString()); + } + else if (type == typeof(PropertyCollection)) + { + textWriter.WriteLine(""); + textWriter.WriteLine("PROPERTIES"); + textWriter.WriteLine("**********"); + + if (propertyValue is PropertyCollection propertyCollection) + { + foreach (var property in propertyCollection) + { + var propProperties = property.GetType().GetProperties(); + + textWriter.WriteLine("Name : " + property.PropertyType.Name); + + foreach (var p in propProperties) + { + if (PropertiesFilter.Contains(p.Name)) continue; + + WritePropertyValue(property, p, textWriter); //recurse + } + + textWriter.WriteLine("------------------------"); + } + } + } + else + { + textWriter.WriteLine(prop.Name + " : " + propertyValue); + } + } + } + + } } diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index 78edd5e9a1..bed5a1e995 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -17,10 +17,10 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; -using umbraco.cms.Actions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; +using Umbraco.Core.Services; namespace Umbraco.Web.Trees { @@ -182,9 +182,12 @@ namespace Umbraco.Web.Trees //add delete option for all members menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); - if (UmbracoContext.Current.Security.CurrentUser.HasAccessToSensitiveData()) + if (Security.CurrentUser.HasAccessToSensitiveData()) { - menu.Items.Add(ui.Text("actions", ActionExportMember.Instance.Alias)); + menu.Items.Add(new MenuItem("export", Services.TextService.Localize("actions/export")) + { + Icon = "download-alt" + }); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e9d00005ba..7dda5bedab 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -40,6 +40,7 @@ ..\ true + latest bin\Debug\ @@ -66,6 +67,7 @@ AllRules.ruleset false Off + latest bin\Release\ diff --git a/src/Umbraco.Web/Umbraco.Web.csproj.DotSettings b/src/Umbraco.Web/Umbraco.Web.csproj.DotSettings index 662f95686e..c54c126d26 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj.DotSettings +++ b/src/Umbraco.Web/Umbraco.Web.csproj.DotSettings @@ -1,2 +1,2 @@  - CSharp50 \ No newline at end of file + CSharp70 \ No newline at end of file diff --git a/src/umbraco.cms/Actions/ActionExportMember.cs b/src/umbraco.cms/Actions/ActionExportMember.cs deleted file mode 100644 index ac00d0ec88..0000000000 --- a/src/umbraco.cms/Actions/ActionExportMember.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using umbraco.BasePages; -using umbraco.interfaces; - -namespace umbraco.cms.Actions -{ - public class ActionExportMember: IAction - { - //create singleton -#pragma warning disable 612, 618 - private static readonly ActionExportMember m_instance = new ActionExportMember(); -#pragma warning restore 612, 618 - - /// - /// A public constructor exists ONLY for backwards compatibility in regards to 3rd party add-ons. - /// All Umbraco assemblies should use the singleton instantiation (this.Instance) - /// When this applicatio is refactored, this constuctor should be made private. - /// - [Obsolete("Use the singleton instantiation instead of a constructor")] - public ActionExportMember() { } - - public static ActionExportMember Instance - { - get { return m_instance; } - } - - #region IAction Members - - public char Letter - { - get - { - return 'E'; - } - } - - public string JsFunctionName - { - get - { - return string.Format("{0}.actionExportMember()", ClientTools.Scripts.GetAppActions); - } - } - - public string JsSource - { - get - { - return null; - } - } - - public string Alias - { - get - { - return "export"; - } - } - - public string Icon - { - get - { - return "download-alt"; - } - } - - public bool ShowInNotifier - { - get - { - return true; - } - } - public bool CanBePermissionAssigned - { - get - { - return true; - } - } - #endregion - - } -} diff --git a/src/umbraco.cms/umbraco.cms.csproj b/src/umbraco.cms/umbraco.cms.csproj index 1f63c9dcd4..42db5b90ee 100644 --- a/src/umbraco.cms/umbraco.cms.csproj +++ b/src/umbraco.cms/umbraco.cms.csproj @@ -183,7 +183,6 @@ -