From 97607bc83b0f027eb65b976b6fe355ec2dd01b40 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 22 Feb 2018 15:32:33 +0100 Subject: [PATCH 1/9] Added front and back end logic to export member data --- src/Umbraco.Core/Models/PropertyType.cs | 3 +- src/Umbraco.Core/Services/IMemberService.cs | 5 +- src/Umbraco.Core/Services/MemberService.cs | 89 ++++++++++++- .../src/common/resources/member.resource.js | 118 +++++++++++------- .../src/views/member/edit.html | 12 +- .../views/member/member.edit.controller.js | 6 + src/Umbraco.Web/Editors/MemberController.cs | 10 ++ 7 files changed, 190 insertions(+), 53 deletions(-) diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 0aebcb6544..492051fca7 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Text.RegularExpressions; @@ -436,4 +435,4 @@ namespace Umbraco.Core.Models return clone; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 9153d23da9..ea214bbe76 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 System.Xml.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -199,6 +200,8 @@ namespace Umbraco.Core.Services /// Id of the MemberType void DeleteMembersOfType(int memberTypeId); + HttpResponseMessage ExportMemberData(Guid key); + [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); @@ -249,4 +252,4 @@ namespace Umbraco.Core.Services /// IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index f22c63e652..1a88f0b714 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Threading; using System.Web.Security; using System.Xml.Linq; @@ -14,6 +15,9 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using Umbraco.Core.Security; namespace Umbraco.Core.Services @@ -253,6 +257,89 @@ namespace Umbraco.Core.Services } } } + /// + /// Exports Member data by unique key + /// + /// + /// + public HttpResponseMessage ExportMemberData(Guid key) + { + + //Filters + List memberPropFilter = new List + { + "RawPasswordValue","ParentId","SortOrder", "Level", "Path", "CreatorId", "Version", "ContentTypeId", "HasIdentity", + "PropertyGroups", "PropertyTypes", "ProviderUserKey", "ContentType" + }; + + List propertiesFilter = new List + { + "PropertyType", "Version", "Id", "HasIdentity", "Key" + }; + + + //Get the member + var member = GetByKey(key); + var memberProperties = member.GetType().GetProperties(); + + string fileName = member.Name + "_" + member.Email + ".txt"; + + using (MemoryStream ms = new MemoryStream()) + { + using (TextWriter tw = new StreamWriter(ms)) + { + foreach (var memberProp in memberProperties) + { + if (memberPropFilter.Contains(memberProp.Name)) continue; + + var propValue = memberProp.GetValue(member, null); + var type = propValue?.GetType(); + + if (type == typeof(PropertyCollection)) + { + tw.WriteLine(""); + tw.WriteLine("PROPERTIES"); + tw.WriteLine("**********"); + + if (propValue is PropertyCollection pc) + foreach (var prop in pc) + { + var propProperties = prop.GetType().GetProperties(); + + //Writing the proerty name + tw.WriteLine("Name : " + prop.PropertyType.Name); + + foreach (var p in propProperties) + { + if (propertiesFilter.Contains(p.Name)) continue; + var pValue = p.GetValue(prop, null); + tw.WriteLine(p.Name + " : " + pValue); + } + + tw.WriteLine("------------------------"); + } + } + else + { + tw.WriteLine(memberProp.Name + " : " + propValue); + } + } + + tw.Flush(); + } + + HttpResponseMessage httpResponseMessage = new HttpResponseMessage(); + httpResponseMessage.Content = new ByteArrayContent(ms.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)] @@ -588,7 +675,7 @@ namespace Umbraco.Core.Services Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMemberRepository(uow); return repository.GetPagedXmlEntriesByPath("-1", pageIndex, pageSize, null, out totalRecords); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index 2073307db9..f98d30ee69 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -10,8 +10,8 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { return umbRequestHelper.postSaveContent({ restApiUrl: umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "PostSave"), + "memberApiBaseUrl", + "PostSave"), content: content, action: action, files: files, @@ -67,35 +67,35 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { } var params = [ - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: toBool(options.orderBySystemField) }, - { filter: options.filter } + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } ]; if (memberTypeAlias != null) { params.push({ memberTypeAlias: memberTypeAlias }); } return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetPagedResults", - params)), - 'Failed to retrieve member paged result'); + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetPagedResults", + params)), + 'Failed to retrieve member paged result'); }, getListNode: function (listName) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetListNodeDisplay", - [{ listName: listName }])), - 'Failed to retrieve data for member list ' + listName); + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetListNodeDisplay", + [{ listName: listName }])), + 'Failed to retrieve data for member list ' + listName); }, /** @@ -122,12 +122,12 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { getByKey: function (key) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetByKey", - [{ key: key }])), - 'Failed to retrieve data for member id ' + key); + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetByKey", + [{ key: key }])), + 'Failed to retrieve data for member id ' + key); }, /** @@ -152,12 +152,12 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { */ deleteByKey: function (key) { return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "DeleteByKey", - [{ key: key }])), - 'Failed to delete item ' + key); + $http.post( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "DeleteByKey", + [{ key: key }])), + 'Failed to delete item ' + key); }, /** @@ -194,20 +194,20 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { if (alias) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }])), - 'Failed to retrieve data for empty member item type ' + alias); + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }])), + 'Failed to retrieve data for empty member item type ' + alias); } else { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty")), - 'Failed to retrieve data for empty member item type ' + alias); + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty")), + 'Failed to retrieve data for empty member item type ' + alias); } }, @@ -242,6 +242,38 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { */ save: function (member, isNew, files) { return saveMember(member, "save" + (isNew ? "New" : ""), files); + }, + + exportMemberData: function (key) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "ExportMemberData", + [{ key: key }])) + .success(function (data, status, headers) { + + headers = headers(); + + var filename = headers['x-filename']; + var contentType = headers['content-type']; + + var linkElement = document.createElement('a'); + + var blob = new Blob([data], { type: contentType }); + var url = window.URL.createObjectURL(blob); + + linkElement.setAttribute('href', url); + linkElement.setAttribute("download", filename); + + var clickEvent = new MouseEvent("click", { + "view": window, + "bubbles": true, + "cancelable": false + }); + + linkElement.dispatchEvent(clickEvent); + })); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index d72750c770..3008786d3a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/edit.html @@ -20,13 +20,13 @@ - + + + + + - - - - - + diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index 215cd7777c..842590c4e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -17,6 +17,7 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS $scope.page.nameLocked = false; $scope.page.listViewPath = null; $scope.page.saveButtonState = "init"; + $scope.page.exportButton = "init"; $scope.busy = false; $scope.page.listViewPath = ($routeParams.page && $routeParams.listName) @@ -171,6 +172,11 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS }; + $scope.export = function() { + var memberKey = $scope.content.key; + memberResource.exportMemberData(memberKey); + } + } angular.module("umbraco").controller("Umbraco.Editors.Member.EditController", MemberEditController); diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 2440ad0691..3ad4d05eb7 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -774,5 +774,15 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } + /// + /// Exports member data + /// + /// + /// + [HttpGet] + public HttpResponseMessage ExportMemberData(Guid key) + { + return Services.MemberService.ExportMemberData(key); + } } } From f0dd32ea19e264d054f0a9be3c8921f038f372b4 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 22 Feb 2018 16:52:16 +0100 Subject: [PATCH 2/9] Removed export placeholder button and added "Action" button instead. --- .../src/views/member/Export.html | 12 +++ .../src/views/member/edit.html | 2 +- .../views/member/member.export.controller.js | 18 ++++ src/Umbraco.Web/Trees/MemberTreeController.cs | 5 +- src/umbraco.cms/Actions/ActionExportMember.cs | 91 +++++++++++++++++++ src/umbraco.cms/umbraco.cms.csproj | 1 + 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/member/Export.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/member/member.export.controller.js create mode 100644 src/umbraco.cms/Actions/ActionExportMember.cs diff --git a/src/Umbraco.Web.UI.Client/src/views/member/Export.html b/src/Umbraco.Web.UI.Client/src/views/member/Export.html new file mode 100644 index 0000000000..0225a51f54 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/member/Export.html @@ -0,0 +1,12 @@ +
+
+ +

+ Are you sure you want to export {{currentNode.name}} details ? +

+ + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index 3008786d3a..154df5dc38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/edit.html @@ -21,7 +21,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.export.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.export.controller.js new file mode 100644 index 0000000000..bd2b3dfee9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.export.controller.js @@ -0,0 +1,18 @@ +function MemberExportController($scope, memberResource, treeService, navigationService, editorState, $location, $routeParams) { + + $scope.performExport = function () { + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + + memberResource.exportMemberData($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + navigationService.hideMenu(); + }); + } + + $scope.cancel = function () { + navigationService.hideDialog(); + }; +} +angular.module("umbraco").controller("Umbraco.Editors.Member.ExportController", MemberExportController); diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index 68f12ef6ca..ce553b6dc4 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -17,6 +17,7 @@ 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; @@ -180,6 +181,8 @@ namespace Umbraco.Web.Trees //add delete option for all members menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); + menu.Items.Add(ui.Text("actions", ActionExportMember.Instance.Alias)); + return menu; } @@ -189,4 +192,4 @@ namespace Umbraco.Web.Trees return _treeSearcher.ExamineSearch(Umbraco, query, UmbracoEntityTypes.Member, pageSize, pageIndex, out totalFound, searchFrom); } } -} \ No newline at end of file +} diff --git a/src/umbraco.cms/Actions/ActionExportMember.cs b/src/umbraco.cms/Actions/ActionExportMember.cs new file mode 100644 index 0000000000..4754ae7c33 --- /dev/null +++ b/src/umbraco.cms/Actions/ActionExportMember.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using umbraco.BasePages; +using umbraco.BusinessLogic.Actions; +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 42db5b90ee..1f63c9dcd4 100644 --- a/src/umbraco.cms/umbraco.cms.csproj +++ b/src/umbraco.cms/umbraco.cms.csproj @@ -183,6 +183,7 @@ + From 8ef3ecd7145447be61f4a77f4ca645b1f580bcbc Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 23 Feb 2018 09:11:42 +0100 Subject: [PATCH 3/9] Forgot to commit the translation keys :| --- src/Umbraco.Web.UI.Client/src/views/member/Export.html | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/member/Export.html b/src/Umbraco.Web.UI.Client/src/views/member/Export.html index 0225a51f54..afda77ba89 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/Export.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/Export.html @@ -2,7 +2,7 @@

- Are you sure you want to export {{currentNode.name}} details ? + Are you sure you want to export {{currentNode.name}} details?

diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index f427a3cc16..61e9b4785d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -11,6 +11,7 @@ Change Document Type Copy Create + Export Create Package Create group Delete @@ -317,7 +318,8 @@ Link title Link Name - Manage hostnames + Name + Export Close this window Are you sure you want to delete Are you sure you want to disable From cff24e6ccf662edbc1654f23c4b683670fad4487 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 23 Feb 2018 09:44:32 +0100 Subject: [PATCH 4/9] Added translation keys for en.xml and deleted duplicate anchorInsert key in en_us.xml --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 ++ src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index ef3e9e37d8..a0b58b37ec 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -11,6 +11,7 @@ Change Document Type Copy Create + Export Create Package Create group Delete @@ -317,6 +318,7 @@ Link title Link Name + Export Manage hostnames Close this window Are you sure you want to delete diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 61e9b4785d..cc8394ce09 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -318,7 +318,6 @@ Link title Link Name - Name Export Close this window Are you sure you want to delete From b9c9a232cdca321ecaa3b3a4787dae0d115d1239 Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Fri, 23 Feb 2018 10:26:11 +0100 Subject: [PATCH 5/9] Style updates, and removes unused usings --- src/Umbraco.Core/Services/MemberService.cs | 70 +++++++++++-------- src/umbraco.cms/Actions/ActionExportMember.cs | 5 -- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 1a88f0b714..fcf9f17502 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -264,72 +264,84 @@ namespace Umbraco.Core.Services /// public HttpResponseMessage ExportMemberData(Guid key) { - - //Filters - List memberPropFilter = new List + var memberPropertyFilter = new List { - "RawPasswordValue","ParentId","SortOrder", "Level", "Path", "CreatorId", "Version", "ContentTypeId", "HasIdentity", - "PropertyGroups", "PropertyTypes", "ProviderUserKey", "ContentType" + "RawPasswordValue", + "ParentId", + "SortOrder", + "Level", + "Path", + "CreatorId", + "Version", + "ContentTypeId", + "HasIdentity", + "PropertyGroups", + "PropertyTypes", + "ProviderUserKey", + "ContentType" }; - List propertiesFilter = new List + var propertiesFilter = new List { - "PropertyType", "Version", "Id", "HasIdentity", "Key" + "PropertyType", + "Version", + "Id", + "HasIdentity", + "Key" }; - - - //Get the member + var member = GetByKey(key); var memberProperties = member.GetType().GetProperties(); + var fileName = $"{member.Name}_{member.Email}.txt"; - string fileName = member.Name + "_" + member.Email + ".txt"; - - using (MemoryStream ms = new MemoryStream()) + using (var memoryStream = new MemoryStream()) { - using (TextWriter tw = new StreamWriter(ms)) + using (var textWriter = new StreamWriter(memoryStream)) { foreach (var memberProp in memberProperties) { - if (memberPropFilter.Contains(memberProp.Name)) continue; + if (memberPropertyFilter.Contains(memberProp.Name)) continue; var propValue = memberProp.GetValue(member, null); var type = propValue?.GetType(); if (type == typeof(PropertyCollection)) { - tw.WriteLine(""); - tw.WriteLine("PROPERTIES"); - tw.WriteLine("**********"); + textWriter.WriteLine(""); + textWriter.WriteLine("PROPERTIES"); + textWriter.WriteLine("**********"); - if (propValue is PropertyCollection pc) - foreach (var prop in pc) + if (propValue is PropertyCollection propertyCollection) + { + foreach (var property in propertyCollection) { - var propProperties = prop.GetType().GetProperties(); + var propProperties = property.GetType().GetProperties(); //Writing the proerty name - tw.WriteLine("Name : " + prop.PropertyType.Name); + textWriter.WriteLine("Name : " + property.PropertyType.Name); foreach (var p in propProperties) { if (propertiesFilter.Contains(p.Name)) continue; - var pValue = p.GetValue(prop, null); - tw.WriteLine(p.Name + " : " + pValue); + var pValue = p.GetValue(property, null); + textWriter.WriteLine(p.Name + " : " + pValue); } - tw.WriteLine("------------------------"); + textWriter.WriteLine("------------------------"); } + } } else { - tw.WriteLine(memberProp.Name + " : " + propValue); + textWriter.WriteLine(memberProp.Name + " : " + propValue); } } - tw.Flush(); + textWriter.Flush(); } - HttpResponseMessage httpResponseMessage = new HttpResponseMessage(); - httpResponseMessage.Content = new ByteArrayContent(ms.ToArray()); + var httpResponseMessage = new HttpResponseMessage(); + 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"); diff --git a/src/umbraco.cms/Actions/ActionExportMember.cs b/src/umbraco.cms/Actions/ActionExportMember.cs index 4754ae7c33..ac00d0ec88 100644 --- a/src/umbraco.cms/Actions/ActionExportMember.cs +++ b/src/umbraco.cms/Actions/ActionExportMember.cs @@ -1,10 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using umbraco.BasePages; -using umbraco.BusinessLogic.Actions; using umbraco.interfaces; namespace umbraco.cms.Actions From 8c5ee547b165f9d15a43d9fff292b20cb927cfda Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Fri, 23 Feb 2018 10:32:49 +0100 Subject: [PATCH 6/9] Does v3 MyGet API work? --- src/NuGet.Config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Config b/src/NuGet.Config index dcccfdd5c1..89e79db976 100644 --- a/src/NuGet.Config +++ b/src/NuGet.Config @@ -2,6 +2,6 @@ - + \ No newline at end of file From 92c0cfb9df0f4a366418ab2e03d3f09ce53c570d Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Fri, 23 Feb 2018 10:58:33 +0100 Subject: [PATCH 7/9] Additional action needs to be counted in the test --- src/Umbraco.Tests/Plugins/PluginManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 22d150ac3b..0cd03df990 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(38, actions.Count()); + Assert.AreEqual(39, actions.Count()); } [Test] From 4850b01097c5af9367cf54f8108f50e7d30af2ad Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 23 Feb 2018 12:23:23 +0100 Subject: [PATCH 8/9] Added checks in place to stop users that has no access to sensitive data to export member data, removed some unused usings --- src/Umbraco.Core/Services/IMemberService.cs | 2 +- src/Umbraco.Core/Services/MemberService.cs | 99 ++++++++++--------- .../src/common/resources/member.resource.js | 2 + src/Umbraco.Web/Editors/MemberController.cs | 4 +- .../Editors/MemberTypeController.cs | 3 - src/Umbraco.Web/Trees/MemberTreeController.cs | 16 +-- 6 files changed, 69 insertions(+), 57 deletions(-) diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index ea214bbe76..2f34a438b3 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -200,7 +200,7 @@ namespace Umbraco.Core.Services /// Id of the MemberType void DeleteMembersOfType(int memberTypeId); - HttpResponseMessage ExportMemberData(Guid key); + HttpResponseMessage ExportMemberData(Guid key, bool hasAccessToSensitive); [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index fcf9f17502..0c3f5e903a 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -262,7 +262,7 @@ namespace Umbraco.Core.Services /// /// /// - public HttpResponseMessage ExportMemberData(Guid key) + public HttpResponseMessage ExportMemberData(Guid key, bool hasAccessToSensitive) { var memberPropertyFilter = new List { @@ -280,7 +280,6 @@ namespace Umbraco.Core.Services "ProviderUserKey", "ContentType" }; - var propertiesFilter = new List { "PropertyType", @@ -289,67 +288,75 @@ namespace Umbraco.Core.Services "HasIdentity", "Key" }; - - var member = GetByKey(key); - var memberProperties = member.GetType().GetProperties(); - var fileName = $"{member.Name}_{member.Email}.txt"; - using (var memoryStream = new MemoryStream()) + var httpResponseMessage = new HttpResponseMessage(); + + if (hasAccessToSensitive) { - using (var textWriter = new StreamWriter(memoryStream)) + var member = GetByKey(key); + + var memberProperties = member.GetType().GetProperties(); + var fileName = $"{member.Name}_{member.Email}.txt"; + + + using (var memoryStream = new MemoryStream()) { - foreach (var memberProp in memberProperties) + using (var textWriter = new StreamWriter(memoryStream)) { - if (memberPropertyFilter.Contains(memberProp.Name)) continue; - - var propValue = memberProp.GetValue(member, null); - var type = propValue?.GetType(); - - if (type == typeof(PropertyCollection)) + foreach (var memberProp in memberProperties) { - textWriter.WriteLine(""); - textWriter.WriteLine("PROPERTIES"); - textWriter.WriteLine("**********"); + if (memberPropertyFilter.Contains(memberProp.Name)) continue; + var propValue = memberProp.GetValue(member, null); + var type = propValue?.GetType(); - if (propValue is PropertyCollection propertyCollection) + if (type == typeof(PropertyCollection)) { - foreach (var property in propertyCollection) + textWriter.WriteLine(""); + textWriter.WriteLine("PROPERTIES"); + textWriter.WriteLine("**********"); + + if (propValue is PropertyCollection propertyCollection) { - var propProperties = property.GetType().GetProperties(); - - //Writing the proerty name - textWriter.WriteLine("Name : " + property.PropertyType.Name); - - foreach (var p in propProperties) + foreach (var property in propertyCollection) { - if (propertiesFilter.Contains(p.Name)) continue; - var pValue = p.GetValue(property, null); - textWriter.WriteLine(p.Name + " : " + pValue); - } + var propProperties = property.GetType().GetProperties(); - textWriter.WriteLine("------------------------"); + 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(memberProp.Name + " : " + propValue); + } } - else - { - textWriter.WriteLine(memberProp.Name + " : " + propValue); - } + + textWriter.Flush(); } - 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; } - - var httpResponseMessage = new HttpResponseMessage(); - 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; } + + httpResponseMessage.StatusCode = HttpStatusCode.Forbidden; + + return httpResponseMessage; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index f98d30ee69..46ca65ad40 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -273,6 +273,8 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { }); linkElement.dispatchEvent(clickEvent); + }).error(function (data, status, header) { + })); } }; diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 3ad4d05eb7..3dc5bdd317 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -782,7 +782,9 @@ namespace Umbraco.Web.Editors [HttpGet] public HttpResponseMessage ExportMemberData(Guid key) { - return Services.MemberService.ExportMemberData(key); + var hasAccessToSensitive = UmbracoContext.Current.Security.CurrentUser.HasAccessToSensitiveData(); + + return Services.MemberService.ExportMemberData(key, hasAccessToSensitive); } } } diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 45486125e7..18bf53a49b 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Web.Security; using AutoMapper; -using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Security; @@ -12,10 +11,8 @@ using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; using System.Web.Http; using System.Net; -using Umbraco.Core.PropertyEditors; using System; using System.Net.Http; -using ContentType = System.Net.Mime.ContentType; namespace Umbraco.Web.Editors { diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index ce553b6dc4..78edd5e9a1 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web.Trees Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)] - [LegacyBaseTree(typeof (loadMembers))] + [LegacyBaseTree(typeof(loadMembers))] [Tree(Constants.Applications.Members, Constants.Trees.Members, null, sortOrder: 0)] [PluginController("UmbracoTrees")] [CoreTree] @@ -55,7 +55,7 @@ namespace Umbraco.Web.Trees /// [HttpQueryStringFilter("queryStrings")] public TreeNode GetTreeNode(string id, FormDataCollection queryStrings) - { + { var node = GetSingleTreeNode(id, queryStrings); //add the tree alias to the node since it is standalone (has no root for which this normally belongs) @@ -117,10 +117,10 @@ namespace Umbraco.Web.Trees "icon-user", false); - return node; + return node; } - + } protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) @@ -174,14 +174,18 @@ namespace Umbraco.Web.Trees createMenuItem.NavigateToRoute("/member/member/edit/-1?create=true"); menu.Items.Add(createMenuItem); } - + menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); return menu; } //add delete option for all members menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); - menu.Items.Add(ui.Text("actions", ActionExportMember.Instance.Alias)); + + if (UmbracoContext.Current.Security.CurrentUser.HasAccessToSensitiveData()) + { + menu.Items.Add(ui.Text("actions", ActionExportMember.Instance.Alias)); + } return menu; From 95aaf40281bf38b82d9289d9d5a339ae0bfb53ae Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Fri, 23 Feb 2018 13:38:21 +0100 Subject: [PATCH 9/9] Some more code cleanup and making sure we pass in the current user so that we can't avoid the sensitivedata check easily --- src/Umbraco.Core/Services/IMemberService.cs | 8 +- src/Umbraco.Core/Services/MemberService.cs | 112 ++++++++++---------- src/Umbraco.Web/Editors/MemberController.cs | 12 +-- 3 files changed, 69 insertions(+), 63 deletions(-) diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 2f34a438b3..d57e110a16 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -200,7 +200,13 @@ namespace Umbraco.Core.Services /// Id of the MemberType void DeleteMembersOfType(int memberTypeId); - HttpResponseMessage ExportMemberData(Guid key, bool hasAccessToSensitive); + /// + /// 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)] diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 0c3f5e903a..71751c4d4e 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -257,13 +257,22 @@ namespace Umbraco.Core.Services } } } + /// - /// Exports Member data by unique key + /// Exports member data based on their unique Id /// - /// - /// - public HttpResponseMessage ExportMemberData(Guid key, bool hasAccessToSensitive) + /// 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", @@ -288,75 +297,66 @@ namespace Umbraco.Core.Services "HasIdentity", "Key" }; + + var member = GetByKey(key); + var memberProperties = member.GetType().GetProperties(); + var fileName = $"{member.Name}_{member.Email}.txt"; - var httpResponseMessage = new HttpResponseMessage(); - - if (hasAccessToSensitive) + using (var memoryStream = new MemoryStream()) { - 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)) { - using (var textWriter = new StreamWriter(memoryStream)) + foreach (var memberProperty in memberProperties) { - foreach (var memberProp in memberProperties) + if (memberPropertyFilter.Contains(memberProperty.Name)) + continue; + + var propertyValue = memberProperty.GetValue(member, null); + var type = propertyValue?.GetType(); + + if (type == typeof(PropertyCollection)) { - if (memberPropertyFilter.Contains(memberProp.Name)) continue; - var propValue = memberProp.GetValue(member, null); - var type = propValue?.GetType(); + textWriter.WriteLine(""); + textWriter.WriteLine("PROPERTIES"); + textWriter.WriteLine("**********"); - if (type == typeof(PropertyCollection)) + if (propertyValue is PropertyCollection propertyCollection) { - textWriter.WriteLine(""); - textWriter.WriteLine("PROPERTIES"); - textWriter.WriteLine("**********"); - - if (propValue is PropertyCollection propertyCollection) + foreach (var property in propertyCollection) { - foreach (var property in propertyCollection) + var propProperties = property.GetType().GetProperties(); + + textWriter.WriteLine("Name : " + property.PropertyType.Name); + + foreach (var p in propProperties) { - 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("------------------------"); + if (propertiesFilter.Contains(p.Name)) continue; + var pValue = p.GetValue(property, null); + textWriter.WriteLine(p.Name + " : " + pValue); } + + textWriter.WriteLine("------------------------"); } } - else - { - textWriter.WriteLine(memberProp.Name + " : " + propValue); - } } - - textWriter.Flush(); + else + { + textWriter.WriteLine(memberProperty.Name + " : " + propertyValue); + } } - 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; + 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; } - - httpResponseMessage.StatusCode = HttpStatusCode.Forbidden; - - return httpResponseMessage; } diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 3dc5bdd317..24035db814 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -774,17 +774,17 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } + /// - /// Exports member data + /// Exports member data based on their unique Id /// - /// - /// + /// The unique member identifier + /// [HttpGet] public HttpResponseMessage ExportMemberData(Guid key) { - var hasAccessToSensitive = UmbracoContext.Current.Security.CurrentUser.HasAccessToSensitiveData(); - - return Services.MemberService.ExportMemberData(key, hasAccessToSensitive); + var currentUser = UmbracoContext.Current.Security.CurrentUser; + return Services.MemberService.ExportMemberData(key, currentUser); } } }