From c2a858b90348f170163fac97c7b1f5dd67bbaab4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 08:55:09 +1100 Subject: [PATCH 01/12] Ensures approval status is checked when validating a member --- .../members/MembersMembershipProvider.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/umbraco.providers/members/MembersMembershipProvider.cs b/src/umbraco.providers/members/MembersMembershipProvider.cs index f6103cc936..aa51e6276c 100644 --- a/src/umbraco.providers/members/MembersMembershipProvider.cs +++ b/src/umbraco.providers/members/MembersMembershipProvider.cs @@ -793,12 +793,10 @@ namespace umbraco.providers.members } } - // TODO: Make approving optional for all member types, forcing it on is not good - - // check for approve status. If not approved, then set the member property to null - //if (!CheckApproveStatus(m)) { - // m = null; - //} + //check for approve status. If not approved, then set the member property to null + if (!CheckApproveStatus(m)) { + m = null; + } // maybe update login date if (m != null && string.IsNullOrEmpty(_lastLoginPropertyTypeAlias) == false) From 7e4739956a5980a23d364156c716c39245b286f3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 14:18:03 +1100 Subject: [PATCH 02/12] Ensures that tree requests have section security applied, though it's not perfect until we have security by tree and section it ensures that a user cannot list out tree data when they don't have access to a section in which that tree's data is required. Now the root node requests via the application tree controller have their auth filters applied (that took quite a lot of zany work). Gets AD login working nicely too. --- .../Editors/AuthenticationController.cs | 8 +- src/Umbraco.Web/Security/WebSecurity.cs | 85 +++++++++++++- .../Trees/ApplicationTreeController.cs | 17 ++- .../Trees/ApplicationTreeExtensions.cs | 53 ++++++++- .../Trees/ContentTreeController.cs | 10 ++ .../Trees/ContentTreeControllerBase.cs | 1 + .../Trees/DataTypeTreeController.cs | 2 + src/Umbraco.Web/Trees/MediaTreeController.cs | 9 ++ src/Umbraco.Web/Trees/MemberTreeController.cs | 7 ++ src/Umbraco.Web/Umbraco.Web.csproj | 3 +- .../WebApi/ControllerContextExtensions.cs | 48 -------- .../WebApi/Filters/FilterGrouping.cs | 56 +++++++++ .../WebApi/HttpControllerContextExtensions.cs | 107 ++++++++++++++++++ 13 files changed, 344 insertions(+), 62 deletions(-) delete mode 100644 src/Umbraco.Web/WebApi/ControllerContextExtensions.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/FilterGrouping.cs create mode 100644 src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 5f0089b9d0..e0bd2ee457 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -4,8 +4,11 @@ using System.Net; using System.Net.Http; using System.Web; using System.Web.Http; +using System.Web.Security; using AutoMapper; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; @@ -13,6 +16,7 @@ using Umbraco.Core.Security; using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; +using umbraco.providers; namespace Umbraco.Web.Editors { @@ -102,7 +106,8 @@ namespace Umbraco.Web.Editors { if (UmbracoContext.Security.ValidateBackOfficeCredentials(username, password)) { - var user = Services.UserService.GetUserByUserName(username); + var user = Security.GetBackOfficeUser(username); + //TODO: Clean up the int cast! var timeoutSeconds = UmbracoContext.Security.PerformLogin((int)user.Id); var result = Mapper.Map(user); @@ -118,6 +123,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.BadRequest); } + /// /// Logs the current user out /// diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index e4312ea994..dce2eba8f0 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -16,6 +16,7 @@ using Umbraco.Web.Models.ContentEditing; using umbraco; using umbraco.DataLayer; using umbraco.businesslogic.Exceptions; +using umbraco.providers; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using Member = umbraco.cms.businesslogic.member.Member; using User = umbraco.BusinessLogic.User; @@ -193,6 +194,88 @@ namespace Umbraco.Web.Security return membershipProvider != null && membershipProvider.ValidateUser(username, password); } + /// + /// Returns the MembershipUser from the back office membership provider + /// + /// + /// + /// + internal MembershipUser GetBackOfficeMembershipUser(string username, bool setOnline) + { + var membershipProvider = Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider]; + return membershipProvider != null ? membershipProvider.GetUser(username, setOnline) : null; + } + + /// + /// Returns the back office IUser instance for the username specified + /// + /// + /// + /// + /// This will return an Iuser instance no matter what membership provider is installed for the back office, it will automatically + /// create any missing Iuser accounts if one is not found and a custom membership provider is being used. + /// + internal IUser GetBackOfficeUser(string username) + { + //get the membership user (set user to be 'online' in the provider too) + var membershipUser = GetBackOfficeMembershipUser(username, true); + + if (membershipUser == null) + { + throw new InvalidOperationException( + "The username & password validated but the membership provider '" + + Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider].Name + + "' did not return a MembershipUser with the username supplied"); + } + + //regarldess of the membership provider used, see if this user object already exists in the umbraco data + var user = _applicationContext.Services.UserService.GetUserByUserName(membershipUser.UserName); + + //we're using the built-in membership provider so the user will already be available + if (Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider] is UsersMembershipProvider) + { + if (user == null) + { + //this should never happen + throw new InvalidOperationException("The user '" + username + "' could not be found in the Umbraco database"); + } + return user; + } + + //we are using a custom membership provider for the back office, in this case we need to create user accounts for the logged in member. + //if we already have a user object in Umbraco we don't need to do anything, otherwise we need to create a mapped Umbraco account. + if (user != null) return user; + + //we need to create an Umbraco IUser of a 'writer' type with access to only content - this was how v6 operates. + var writer = _applicationContext.Services.UserService.GetUserTypeByAlias("writer"); + + var email = membershipUser.Email; + if (email.IsNullOrWhiteSpace()) + { + //in some cases if there is no email we have to generate one since it is required! + email = Guid.NewGuid().ToString("N") + "@example.com"; + } + + user = new Core.Models.Membership.User(writer) + { + Email = email, + Language = GlobalSettings.DefaultUILanguage, + Name = membershipUser.UserName, + Password = Guid.NewGuid().ToString("N"), //Need to set this to something - will not be used though + DefaultPermissions = writer.Permissions, + Username = membershipUser.UserName, + StartContentId = -1, + StartMediaId = -1, + NoConsole = false, + IsApproved = true + }; + user.AddAllowedSection("content"); + + _applicationContext.Services.UserService.SaveUser(user); + + return user; + } + /// /// Changes password for a member/user given the membership provider and the password change model /// @@ -496,7 +579,7 @@ namespace Umbraco.Web.Security { } } - + protected override void DisposeResources() { _httpContext = null; diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 5b18e5698b..f5ef1651ca 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Management.Instrumentation; using System.Net; using System.Net.Http.Formatting; +using System.Threading.Tasks; using System.Web.Http; using System.Web.Mvc; using Umbraco.Core; @@ -42,7 +43,7 @@ namespace Umbraco.Web.Trees /// /// [HttpQueryStringFilter("queryStrings")] - public SectionRootNode GetApplicationTrees(string application, string tree, FormDataCollection queryStrings) + public async Task GetApplicationTrees(string application, string tree, FormDataCollection queryStrings) { if (string.IsNullOrEmpty(application)) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -59,14 +60,12 @@ namespace Umbraco.Web.Trees if (apptree == null) throw new HttpResponseException(HttpStatusCode.NotFound); - var result = GetRootForSingleAppTree( + var result = await GetRootForSingleAppTree( apptree, Constants.System.Root.ToString(CultureInfo.InvariantCulture), queryStrings, application); - ////PP: should this be further down in the logic? - //result.Title = ui.Text("sections", application); return result; } @@ -75,7 +74,7 @@ namespace Umbraco.Web.Trees foreach (var apptree in appTrees) { //return the root nodes for each tree in the app - var rootNode = GetRootForMultipleAppTree(apptree, queryStrings); + var rootNode = await GetRootForMultipleAppTree(apptree, queryStrings); //This could be null if the tree decides not to return it's root (i.e. the member type tree does this when not in umbraco membership mode) if (rootNode != null) { @@ -94,10 +93,10 @@ namespace Umbraco.Web.Trees /// /// /// - private TreeNode GetRootForMultipleAppTree(ApplicationTree configTree, FormDataCollection queryStrings) + private async Task GetRootForMultipleAppTree(ApplicationTree configTree, FormDataCollection queryStrings) { if (configTree == null) throw new ArgumentNullException("configTree"); - var byControllerAttempt = configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext); + var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext); if (byControllerAttempt.Success) { return byControllerAttempt.Result; @@ -119,14 +118,14 @@ namespace Umbraco.Web.Trees /// /// /// - private SectionRootNode GetRootForSingleAppTree(ApplicationTree configTree, string id, FormDataCollection queryStrings, string application) + private async Task GetRootForSingleAppTree(ApplicationTree configTree, string id, FormDataCollection queryStrings, string application) { var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture); if (configTree == null) throw new ArgumentNullException("configTree"); var byControllerAttempt = configTree.TryLoadFromControllerTree(id, queryStrings, ControllerContext); if (byControllerAttempt.Success) { - var rootNode = configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext); + var rootNode = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext); if (rootNode.Success == false) { //This should really never happen if we've successfully got the children above. diff --git a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs index 72c58f7e4d..8142637d02 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs @@ -6,15 +6,20 @@ using System.Management.Instrumentation; using System.Net.Http; using System.Net.Http.Formatting; using System.Text; +using System.Threading; using System.Threading.Tasks; +using System.Web.Http; using System.Web.Http.Controllers; +using System.Web.Http.Routing; using System.Web.Mvc; using Umbraco.Core; using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using umbraco.BusinessLogic; using umbraco.cms.presentation.Trees; using ApplicationTree = Umbraco.Core.Models.ApplicationTree; +using IAuthorizationFilter = System.Web.Http.Filters.IAuthorizationFilter; using UrlHelper = System.Web.Http.Routing.UrlHelper; namespace Umbraco.Web.Trees @@ -38,18 +43,61 @@ namespace Umbraco.Web.Trees return Attempt.Succeed(foundControllerTree); } - internal static Attempt TryGetRootNodeFromControllerTree(this ApplicationTree appTree, FormDataCollection formCollection, HttpControllerContext controllerContext) + /// + /// This will go and get the root node from a controller tree by executing the tree's GetRootNode method + /// + /// + /// + /// + /// + /// + /// This ensures that authorization filters are applied to the sub request + /// + internal static async Task> TryGetRootNodeFromControllerTree(this ApplicationTree appTree, FormDataCollection formCollection, HttpControllerContext controllerContext) { var foundControllerTreeAttempt = appTree.TryGetControllerTree(); if (foundControllerTreeAttempt.Success == false) { return Attempt.Fail(foundControllerTreeAttempt.Exception); } + var foundControllerTree = foundControllerTreeAttempt.Result; //instantiate it, since we are proxying, we need to setup the instance with our current context var instance = (TreeController)DependencyResolver.Current.GetService(foundControllerTree); - instance.ControllerContext = controllerContext; + + //NOTE: This is all required in order to execute the auth-filters for the sub request, we + // need to "trick" web-api into thinking that it is actually executing the proxied controller. + + var urlHelper = controllerContext.Request.GetUrlHelper(); + //create the proxied URL for the controller action + var proxiedUrl = controllerContext.Request.RequestUri.GetLeftPart(UriPartial.Authority) + + urlHelper.GetUmbracoApiService("GetRootNode", instance.GetType()); + //add the query strings to it + proxiedUrl += "?" + formCollection.ToQueryString(); + //create proxy route data specifying the action / controller to execute + var proxiedRouteData = new HttpRouteData( + controllerContext.RouteData.Route, + new HttpRouteValueDictionary(new {action = "GetRootNode", controller = ControllerExtensions.GetControllerName(instance.GetType())})); + //create a proxied controller context + var proxiedControllerContext = new HttpControllerContext( + controllerContext.Configuration, + proxiedRouteData, + new HttpRequestMessage(HttpMethod.Get, proxiedUrl)) + { + ControllerDescriptor = new HttpControllerDescriptor(controllerContext.ControllerDescriptor.Configuration, ControllerExtensions.GetControllerName(instance.GetType()), instance.GetType()) + }; + + instance.ControllerContext = proxiedControllerContext; instance.Request = controllerContext.Request; + + //invoke auth filters for this sub request + var result = await instance.ControllerContext.InvokeAuthorizationFiltersForRequest(); + //if a result is returned it means they are unauthorized, just throw the response. + if (result != null) + { + throw new HttpResponseException(result); + } + //return the root var node = instance.GetRootNode(formCollection); return node == null @@ -193,4 +241,5 @@ namespace Umbraco.Web.Trees } } + } diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 7b43afd0e7..9c389416fb 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Services; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; using umbraco.businesslogic; @@ -20,6 +21,15 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { + //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered + // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. + [UmbracoApplicationAuthorize( + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Users, + Constants.Applications.Settings, + Constants.Applications.Developer, + Constants.Applications.Members)] [LegacyBaseTree(typeof(loadContent))] [Tree(Constants.Applications.Content, Constants.Trees.Content, "Content")] [PluginController("UmbracoTrees")] diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index c1e6be9196..6b9b46e0bf 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -83,6 +83,7 @@ namespace Umbraco.Web.Trees //check if a request has been made to render from a specific start node + //TODO: This 'undefined' check should not be required whatseover - this parameter should not be sent up ever it if is null from the front-end. if (!string.IsNullOrEmpty(altStartId) && altStartId != "undefined" && altStartId != Constants.System.Root.ToString(CultureInfo.InvariantCulture)) { id = queryStrings.GetValue(TreeQueryStringParameters.StartNodeId); diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index c41deb350b..c428bdcc94 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -7,12 +7,14 @@ using System.Web.Http; using Umbraco.Core; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { + [UmbracoApplicationAuthorize(Constants.Applications.Developer)] [Tree(Constants.Applications.Developer, Constants.Trees.DataTypes, "Data Types")] [PluginController("UmbracoTrees")] [CoreTree] diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 15ee269434..2938c6ab47 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -9,12 +9,21 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { + //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered + // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. + [UmbracoApplicationAuthorize( + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Settings, + Constants.Applications.Developer, + Constants.Applications.Members)] [LegacyBaseTree(typeof(loadMedia))] [Tree(Constants.Applications.Media, Constants.Trees.Media, "Media")] [PluginController("UmbracoTrees")] diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index af783b489b..5fd08c4bf9 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -6,6 +6,7 @@ using System.Web.Security; using Umbraco.Core; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; using umbraco.cms.businesslogic.member; @@ -15,6 +16,12 @@ namespace Umbraco.Web.Trees { //TODO: Upgrade thsi to use the new Member Service! + //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered + // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. + [UmbracoApplicationAuthorize( + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Members)] [LegacyBaseTree(typeof (loadMembers))] [Tree(Constants.Applications.Members, Constants.Trees.Members, "Members")] [PluginController("UmbracoTrees")] diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 652a5d3ffb..95a5688d99 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -435,7 +435,8 @@ Reference.map - + + diff --git a/src/Umbraco.Web/WebApi/ControllerContextExtensions.cs b/src/Umbraco.Web/WebApi/ControllerContextExtensions.cs deleted file mode 100644 index 590785f188..0000000000 --- a/src/Umbraco.Web/WebApi/ControllerContextExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Runtime.Remoting.Contexts; -using System.Web.Http.Controllers; - -namespace Umbraco.Web.WebApi -{ - internal static class ControllerContextExtensions - { - /// - /// Sets the JSON GUID format to not have hyphens - /// - /// - internal static void SetOutgoingNoHyphenGuidFormat(this HttpControllerContext controllerContext) - { - var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; - jsonFormatter.SerializerSettings.Converters.Add(new GuidNoHyphenConverter()); - } - - - /// - /// Sets the JSON datetime format to be a custom one - /// - /// - /// - internal static void SetOutgoingDateTimeFormat(this HttpControllerContext controllerContext, string format) - { - var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; - jsonFormatter.SerializerSettings.Converters.Add(new CustomDateTimeConvertor(format)); - } - - /// - /// Sets the JSON datetime format to be our regular iso standard - /// - internal static void SetOutgoingDateTimeFormat(this HttpControllerContext controllerContext) - { - var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; - jsonFormatter.SerializerSettings.Converters.Add(new CustomDateTimeConvertor("yyyy-MM-dd HH:mm:ss")); - } - - /// - /// Removes the xml formatter so it only outputs json - /// - /// - internal static void EnsureJsonOutputOnly(this HttpControllerContext controllerContext) - { - controllerContext.Configuration.Formatters.Remove(controllerContext.Configuration.Formatters.XmlFormatter); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/FilterGrouping.cs b/src/Umbraco.Web/WebApi/Filters/FilterGrouping.cs new file mode 100644 index 0000000000..581579a8d1 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/FilterGrouping.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Http.Filters; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Quickly split filters into different types + /// + internal class FilterGrouping + { + private readonly List _actionFilters = new List(); + private readonly List _authorizationFilters = new List(); + private readonly List _exceptionFilters = new List(); + + public FilterGrouping(IEnumerable filters) + { + if (filters == null) throw new ArgumentNullException("filters"); + + foreach (FilterInfo f in filters) + { + var filter = f.Instance; + Categorize(filter, _actionFilters); + Categorize(filter, _authorizationFilters); + Categorize(filter, _exceptionFilters); + } + } + + public IEnumerable ActionFilters + { + get { return _actionFilters; } + } + + public IEnumerable AuthorizationFilters + { + get { return _authorizationFilters; } + } + + public IEnumerable ExceptionFilters + { + get { return _exceptionFilters; } + } + + private static void Categorize(IFilter filter, List list) where T : class + { + T match = filter as T; + if (match != null) + { + list.Add(match); + } + } + } +} diff --git a/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs b/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs new file mode 100644 index 0000000000..f7fe8e1bbe --- /dev/null +++ b/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.Remoting.Contexts; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.WebApi +{ + internal static class HttpControllerContextExtensions + { + /// + /// This method will go an execute the authorization filters for the controller action, if any fail + /// it will return their response, otherwise we'll return null. + /// + /// + internal static async Task InvokeAuthorizationFiltersForRequest(this HttpControllerContext controllerContext) + { + var controllerDescriptor = controllerContext.ControllerDescriptor; + var controllerServices = controllerDescriptor.Configuration.Services; + var actionDescriptor = controllerServices.GetActionSelector().SelectAction(controllerContext); + var actionContext = new HttpActionContext(controllerContext, actionDescriptor); + var filters = actionDescriptor.GetFilterPipeline(); + var filterGrouping = new FilterGrouping(filters); + var actionFilters = filterGrouping.ActionFilters; + // Because the continuation gets built from the inside out we need to reverse the filter list + // so that least specific filters (Global) get run first and the most specific filters (Action) get run last. + var authorizationFilters = filterGrouping.AuthorizationFilters.Reverse().ToArray(); + + if (authorizationFilters.Any()) + { + var cancelToken = new CancellationToken(); + var filterResult = await FilterContinuation(actionContext, cancelToken, authorizationFilters, 0); + if (filterResult != null) + { + //this means that the authorization filter has returned a result - unauthorized so we cannot continue + return filterResult; + } + } + return null; + } + + /// + /// This method is how you execute a chain of filters, it needs to recursively call in to itself as the continuation for the next filter in the chain + /// + /// + /// + /// + /// + /// + private static async Task FilterContinuation(HttpActionContext actionContext, CancellationToken token, IList filters, int index) + { + return await filters[index].ExecuteAuthorizationFilterAsync(actionContext, token, () => + { + Func nullResponse = () => null; + return (index + 1) == filters.Count + ? Task.Run(nullResponse) + : FilterContinuation(actionContext, token, filters, ++index); + }); + } + + /// + /// Sets the JSON GUID format to not have hyphens + /// + /// + internal static void SetOutgoingNoHyphenGuidFormat(this HttpControllerContext controllerContext) + { + var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; + jsonFormatter.SerializerSettings.Converters.Add(new GuidNoHyphenConverter()); + } + + + /// + /// Sets the JSON datetime format to be a custom one + /// + /// + /// + internal static void SetOutgoingDateTimeFormat(this HttpControllerContext controllerContext, string format) + { + var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; + jsonFormatter.SerializerSettings.Converters.Add(new CustomDateTimeConvertor(format)); + } + + /// + /// Sets the JSON datetime format to be our regular iso standard + /// + internal static void SetOutgoingDateTimeFormat(this HttpControllerContext controllerContext) + { + var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; + jsonFormatter.SerializerSettings.Converters.Add(new CustomDateTimeConvertor("yyyy-MM-dd HH:mm:ss")); + } + + /// + /// Removes the xml formatter so it only outputs json + /// + /// + internal static void EnsureJsonOutputOnly(this HttpControllerContext controllerContext) + { + controllerContext.Configuration.Formatters.Remove(controllerContext.Configuration.Formatters.XmlFormatter); + } + } +} \ No newline at end of file From b52b6495483bee61e87ab87bd3aca118f462d198 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 14:41:05 +1100 Subject: [PATCH 03/12] Fixes: U4-3209 Ensure user is validated for PostAddFile on MediaController --- src/Umbraco.Web/Editors/MediaController.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 321704e828..7ccfaabd92 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -472,6 +472,15 @@ namespace Umbraco.Web.Editors }); } + //ensure the user has access to this folder by parent id! + if (CheckPermissions( + new Dictionary(), + Security.CurrentUser, + Services.MediaService, parentId) == false) + { + return new HttpResponseMessage(HttpStatusCode.Unauthorized); + } + //get the files foreach (var file in result.FileData) { From 2f089128d71cc3f390030d35166fdd1743d1930a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 14:48:11 +1100 Subject: [PATCH 04/12] Fixes: U4-3608 When logged in as a user with a custom start node the media thumbs do not display on a folder --- .../WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs index b72662e224..c93ed87a21 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -77,7 +77,7 @@ namespace Umbraco.Web.WebApi.Filters var toRemove = new List(); foreach (dynamic item in items) { - var hasPathAccess = (item != null && UserExtensions.HasPathAccess(item.Path, user.StartContentId, Constants.System.RecycleBinContent)); + var hasPathAccess = (item != null && UserExtensions.HasPathAccess(item.Path, user.StartMediaId, Constants.System.RecycleBinMedia)); if (!hasPathAccess) { toRemove.Add(item); From 596cf2a9e184fe9966dc96daa9d37a980288af39 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 15:11:08 +1100 Subject: [PATCH 05/12] Ensures that members are auto-approved if creating a member via the legacy members API. --- .../members/MembersMembershipProvider.cs | 39 ++++++++++++++++++- .../umbraco.providers.csproj | 4 ++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/umbraco.providers/members/MembersMembershipProvider.cs b/src/umbraco.providers/members/MembersMembershipProvider.cs index aa51e6276c..e6eb61d018 100644 --- a/src/umbraco.providers/members/MembersMembershipProvider.cs +++ b/src/umbraco.providers/members/MembersMembershipProvider.cs @@ -849,7 +849,16 @@ namespace umbraco.providers.members var approveStatus = GetMemberProperty(m, ApprovedPropertyTypeAlias, true); if (string.IsNullOrEmpty(approveStatus) == false) { - bool.TryParse(approveStatus, out isApproved); + //try parsing as bool first (just in case) + if (bool.TryParse(approveStatus, out isApproved) == false) + { + int intStatus; + //if that fails, try parsing as int (since its normally stored as 0 or 1) + if (int.TryParse(approveStatus, out intStatus)) + { + isApproved = intStatus != 0; + } + } } } } @@ -1015,4 +1024,32 @@ namespace umbraco.providers.members #endregion } + + /// + /// Adds some event handling + /// + public class MembershipEventHandler : ApplicationEventHandler + { + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + Member.New += Member_New; + } + + void Member_New(Member sender, NewEventArgs e) + { + //This is a bit of a hack to ensure that the member is approved when created since many people will be using + // this old api to create members on the front-end and they need to be approved - which is based on whether or not + // the Umbraco membership provider is configured. + var provider = Membership.Provider as UmbracoMembershipProvider; + if (provider != null) + { + var approvedField = provider.ApprovedPropertyTypeAlias; + var property = sender.getProperty(approvedField); + if (property != null) + { + property.Value = 1; + } + } + } + } } diff --git a/src/umbraco.providers/umbraco.providers.csproj b/src/umbraco.providers/umbraco.providers.csproj index 41c1457c7d..8d96f0e181 100644 --- a/src/umbraco.providers/umbraco.providers.csproj +++ b/src/umbraco.providers/umbraco.providers.csproj @@ -107,6 +107,10 @@ {C7CB79F0-1C97-4B33-BFA7-00731B579AE2} umbraco.datalayer + + {511F6D8D-7717-440A-9A57-A507E9A8B27F} + umbraco.interfaces + From 0c226d1a2c170e7f3888aae67dd3de85af9e2a79 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 15:54:50 +1100 Subject: [PATCH 06/12] Fixes: U4-3581 Make sure advanced v7 prop editors can also be used from xslt --- src/Umbraco.Tests/LibraryTests.cs | 46 ++++++++++++++++++- .../umbraco.presentation/library.cs | 26 +++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Tests/LibraryTests.cs b/src/Umbraco.Tests/LibraryTests.cs index fd8451443c..55cc9d2a02 100644 --- a/src/Umbraco.Tests/LibraryTests.cs +++ b/src/Umbraco.Tests/LibraryTests.cs @@ -62,7 +62,51 @@ namespace Umbraco.Tests get { return DatabaseBehavior.NoDatabasePerFixture; } } - [Test] + [Test] + public void Json_To_Xml_Object() + { + var json = "{ id: 1, name: 'hello', children: [{id: 2, name: 'child1'}, {id:3, name: 'child2'}]}"; + var result = library.JsonToXml(json); + Assert.AreEqual(@" + 1 + hello + + 2 + child1 + + + 3 + child2 + +", result.Current.OuterXml); + } + + [Test] + public void Json_To_Xml_Array() + { + var json = "[{id: 2, name: 'child1'}, {id:3, name: 'child2'}]"; + var result = library.JsonToXml(json); + Assert.AreEqual(@" + + 2 + child1 + + + 3 + child2 + +", result.Current.OuterXml); + } + + [Test] + public void Json_To_Xml_Error() + { + var json = "{ id: 1, name: 'hello', children: }"; + var result = library.JsonToXml(json); + Assert.IsTrue(result.Current.OuterXml.StartsWith("")); + } + + [Test] public void Get_Item_User_Property() { var val = library.GetItem(1173, "content"); diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index 629fd9eb8b..5cc94d4020 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -10,6 +10,7 @@ using System.Web; using System.Web.UI; using System.Xml; using System.Xml.XPath; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -235,6 +236,31 @@ namespace umbraco #region Xslt Helper functions + /// + /// This will convert a json structure to xml for use in xslt + /// + /// + /// + public static XPathNodeIterator JsonToXml(string json) + { + try + { + if (json.StartsWith("[")) + { + //we'll assume it's an array, in which case we need to add a root + json = "{\"arrayitem\":" + json + "}"; + } + var xml = JsonConvert.DeserializeXmlNode(json, "json", false); + return xml.CreateNavigator().Select("/"); + } + catch (Exception ex) + { + var xd = new XmlDocument(); + xd.LoadXml(string.Format("Could not convert json to xml. Error: {0}", ex)); + return xd.CreateNavigator().Select("/"); + } + } + /// /// Add a session variable to the current user /// From 0a11f31754927f715930723725afacb81883e314 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 16:58:25 +1100 Subject: [PATCH 07/12] Fixes: U4-3610 When adding another tab to a member type no members will load and result in a YSOD --- .../Relators/PropertyTypePropertyGroupRelator.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Relators/PropertyTypePropertyGroupRelator.cs b/src/Umbraco.Core/Persistence/Relators/PropertyTypePropertyGroupRelator.cs index 29342e283f..bf307ea594 100644 --- a/src/Umbraco.Core/Persistence/Relators/PropertyTypePropertyGroupRelator.cs +++ b/src/Umbraco.Core/Persistence/Relators/PropertyTypePropertyGroupRelator.cs @@ -19,11 +19,17 @@ namespace Umbraco.Core.Persistence.Relators // Is this the same MemberTypeReadOnlyDto as the current one we're processing if (Current != null && Current.UniqueId == a.UniqueId) { - // Yes, just add this PropertyTypeReadOnlyDto to the current MemberTypeReadOnlyDto's collection - Current.PropertyTypes.Add(p); - + //This property may already be added so we need to check for that + if (p.Id.HasValue && Current.PropertyTypes.Any(x => x.Id == p.Id.Value) == false) + { + // Add this PropertyTypeReadOnlyDto to the current MemberTypeReadOnlyDto's collection + Current.PropertyTypes.Add(p); + } + if (g.Id.HasValue && Current.PropertyTypeGroups != null && Current.PropertyTypeGroups.Any(x => x.Id == g.Id.Value) == false) + { Current.PropertyTypeGroups.Add(g); + } // Return null to indicate we're not done with this MemberTypeReadOnlyDto yet return null; @@ -46,7 +52,9 @@ namespace Umbraco.Core.Persistence.Relators Current.PropertyTypeGroups = new List(); if (g.Id.HasValue) + { Current.PropertyTypeGroups.Add(g); + } // Return the now populated previous MemberTypeReadOnlyDto (or null if first time through) return prev; From d87db5b3385588fcb7f5469e491373771c316eda Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 17:24:45 +1100 Subject: [PATCH 08/12] Shows an error notification if there is a ysod when not in debug mode so the user at least knows something has gone wrong. --- .../services/umbrequesthelper.service.js | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 9de4e0c541..1398deca20 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -3,7 +3,7 @@ * @name umbraco.services.umbRequestHelper * @description A helper object used for sending requests to the server **/ -function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService) { +function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService) { return { /** @@ -133,12 +133,20 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ var result = callbacks.error.apply(this, [data, status, headers, config]); //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. - if (status >= 500 && status < 600 && Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { + if (status >= 500 && status < 600) { - dialogService.ysodDialog({ - errorMsg: result.errorMsg, - data: result.data - }); + //show a ysod dialog + if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { + dialogService.ysodDialog({ + errorMsg: result.errorMsg, + data: result.data + }); + } + else { + //show a simple error notification + notificationsService.error("Server error", "Contact administrator, see log for full details.
" + result.errorMsg + ""); + } + } else { @@ -214,12 +222,20 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ //failure callback //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. - if (status >= 500 && status < 600 && Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { + if (status >= 500 && status < 600) { - dialogService.ysodDialog({ - errorMsg: 'An error occurred', - data: data - }); + //show a ysod dialog + if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { + dialogService.ysodDialog({ + errorMsg: 'An error occurred', + data: data + }); + } + else { + //show a simple error notification + notificationsService.error("Server error", "Contact administrator, see log for full details.
" + data.ExceptionMessage + ""); + } + } else { From d1a656b3cd3fcb66fb0fc8f9a233530fe2b64a58 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 17:25:25 +1100 Subject: [PATCH 09/12] FIxes installer when duplicate conflicts: U4-3612 Cannot install Txt starter kit in the back office --- .../businesslogic/Packager/Installer.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/umbraco.cms/businesslogic/Packager/Installer.cs b/src/umbraco.cms/businesslogic/Packager/Installer.cs index 49247e49ce..ef9b77e16b 100644 --- a/src/umbraco.cms/businesslogic/Packager/Installer.cs +++ b/src/umbraco.cms/businesslogic/Packager/Installer.cs @@ -626,7 +626,10 @@ namespace umbraco.cms.businesslogic.packager if (m != null) { ContainsMacroConflict = true; - _conflictingMacroAliases.Add(m.Name, alias); + if (_conflictingMacroAliases.ContainsKey(m.Name) == false) + { + _conflictingMacroAliases.Add(m.Name, alias); + } } } } @@ -639,8 +642,11 @@ namespace umbraco.cms.businesslogic.packager var t = Template.GetByAlias(alias); if (t != null) { - this.ContainsTemplateConflicts = true; - this._conflictingTemplateAliases.Add(t.Text, alias); + ContainsTemplateConflicts = true; + if (_conflictingTemplateAliases.ContainsKey(t.Text) == false) + { + _conflictingTemplateAliases.Add(t.Text, alias); + } } } } @@ -653,8 +659,11 @@ namespace umbraco.cms.businesslogic.packager var s = StyleSheet.GetByName(alias); if (s != null) { - this.ContainsStyleSheeConflicts = true; - this._conflictingStyleSheetNames.Add(s.Text, alias); + ContainsStyleSheeConflicts = true; + if (_conflictingStyleSheetNames.ContainsKey(s.Text) == false) + { + _conflictingStyleSheetNames.Add(s.Text, alias); + } } } } From 39aa100490028562f2770686105d5c1976fa6a6d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 18:10:54 +1100 Subject: [PATCH 10/12] Fixes: U4-3572 CSS class changes are not applied to tree nodes after resync --- .../directives/umbtreeitem.directive.js | 24 +++++++++++++++---- .../src/common/services/navigation.service.js | 4 ++-- .../src/common/services/tree.service.js | 13 ++++++++-- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js index 21324d2e68..0b56f08a1f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js @@ -54,6 +54,15 @@ angular.module("umbraco.directives") } } + /** updates the node's styles */ + function styleNode(node) { + node.stateCssClass = (node.cssClasses || []).join(" "); + + if (node.style) { + $(element).find("i").attr("style", node.style); + } + } + /** This will deleteAnimations to true after the current digest */ function enableDeleteAnimations() { //do timeout so that it re-enables them after this digest @@ -63,6 +72,15 @@ angular.module("umbraco.directives") }, 0, false); } + //add a method to the node which we can use to call to update the node data if we need to , + // this is done by sync tree, we don't want to add a $watch for each node as that would be crazy insane slow + // so we have to do this + scope.node.updateNodeData = function (newNode) { + _.extend(scope.node, newNode); + //now update the styles + styleNode(scope.node); + }; + /** Method called when the options button next to a node is called In the main tree this opens the menu, but internally the tree doesnt @@ -153,12 +171,8 @@ angular.module("umbraco.directives") }; //if the current path contains the node id, we will auto-expand the tree item children - - scope.node.stateCssClass = (scope.node.cssClasses || []).join(" "); - if(scope.node.style){ - $(element).find("i").attr("style", scope.node.style); - } + styleNode(scope.node); var template = '
'; var newElement = angular.element(template); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index e4bbd4892b..ac39e82b8b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -272,7 +272,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo }, /** * @ngdoc method - * @name umbraco.services.navigationService#syncTreePath + * @name umbraco.services.navigationService#syncTree * @methodOf umbraco.services.navigationService * * @description @@ -280,7 +280,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo * The path format is: ["itemId","itemId"], and so on * so to sync to a specific document type node do: *
-         * navigationService.syncTreePath({tree: 'content', path: ["-1","123d"], forceReload: true});  
+         * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});  
          * 
* @param {Object} args arguments passed to the function * @param {String} args.tree the tree alias to sync to diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 44e405ef76..ffa81967ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -592,8 +592,17 @@ function treeService($q, treeResource, iconHelper, notificationsService, $rootSc var index = _.indexOf(node.parent().children, node); //the trick here is to not actually replace the node - this would cause the delete animations //to fire, instead we're just going to replace all the properties of this node. - _.extend(node.parent().children[index], found); - //set the node to loading + + //there should always be a method assigned but we'll check anyways + if (angular.isFunction(node.parent().children[index].updateNodeData)) { + node.parent().children[index].updateNodeData(found); + } + else { + //just update as per normal - this means styles, etc.. won't be applied + _.extend(node.parent().children[index], found); + } + + //set the node loading node.parent().children[index].loading = false; //return deferred.resolve(node.parent().children[index]); From 84353d1e77cebf74c7168dcc31a0fc5e4938ef21 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2013 18:23:23 +1100 Subject: [PATCH 11/12] Fixes: U4-3587 Can't delete a member when there is a group assigned --- src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 6175c2d494..2a1a6777ab 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -173,6 +173,7 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM umbracoRelation WHERE childId = @Id", "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsMember2MemberGroup WHERE Member = @Id", "DELETE FROM cmsMember WHERE nodeId = @Id", "DELETE FROM cmsContentVersion WHERE ContentId = @Id", "DELETE FROM cmsContentXml WHERE nodeID = @Id", From a626eeff7d1d80fc526f184b3957fb4c0b5e94d2 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Nov 2013 09:57:34 +0100 Subject: [PATCH 12/12] U4-3591 - more explicit exception when two IPropertyValueConverter collides --- .../Models/PublishedContent/PublishedPropertyType.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index b94a21a9bd..0bd51784bf 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -92,8 +92,11 @@ namespace Umbraco.Core.Models.PublishedContent } else { - throw new InvalidOperationException(string.Format("More than one converter for property type {0}.{1}", - ContentType.Alias, PropertyTypeAlias)); + throw new InvalidOperationException(string.Format("Type '{2}' cannot be an IPropertyValueConverter" + + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" + + " for that property, and only one converter can exist for a property.", + ContentType.Alias, PropertyTypeAlias, + converter.GetType().FullName, _converter.GetType().FullName)); } }