diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 8c10fc6bd0..49829d984e 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -131,42 +132,7 @@ namespace Umbraco.Web.Editors }) .ToList(); } - - /// - /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors - /// - /// - /// - /// - protected void ValidateComposition(ContentTypeSave contentTypeSave, IContentTypeComposition composition) - where TPropertyType : PropertyTypeBasic - { - var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); - if (validateAttempt == false) - { - //if it's not successful then we need to return some model state for the property aliases that - // are duplicated - var propertyAliases = validateAttempt.Result.Distinct(); - foreach (var propertyAlias in propertyAliases) - { - //find the property relating to these - var prop = contentTypeSave.Groups.SelectMany(x => x.Properties).Single(x => x.Alias == propertyAlias); - var group = contentTypeSave.Groups.Single(x => x.Properties.Contains(prop)); - var propIndex = group.Properties.IndexOf(prop); - var groupIndex = contentTypeSave.Groups.IndexOf(group); - - var key = string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propIndex); - ModelState.AddModelError(key, "Duplicate property aliases not allowed between compositions"); - } - - var display = Mapper.Map(composition); - //map the 'save' data on top - display = Mapper.Map(contentTypeSave, display); - display.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - - } + protected string TranslateItem(string text) { @@ -186,7 +152,6 @@ namespace Umbraco.Web.Editors TContentTypeSave contentTypeSave, Func getContentType, Action saveContentType, - bool validateComposition = true, Action beforeCreateNew = null) where TContentType : class, IContentTypeComposition where TContentTypeDisplay : ContentTypeCompositionDisplay @@ -213,22 +178,7 @@ namespace Umbraco.Web.Editors if (ModelState.IsValid == false) { - TContentTypeDisplay forDisplay; - if (ctId > 0) - { - //Required data is invalid so we cannot continue - forDisplay = Mapper.Map(ct); - //map the 'save' data on top - forDisplay = Mapper.Map(contentTypeSave, forDisplay); - } - else - { - //map the 'save' data to display - forDisplay = Mapper.Map(contentTypeSave); - } - - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + throw CreateModelStateValidationException(ctId, contentTypeSave, ct); } //filter out empty properties @@ -240,15 +190,21 @@ namespace Umbraco.Web.Editors if (ctId > 0) { - //its an update to an existing + //its an update to an existing content type - Mapper.Map(contentTypeSave, ct); - - if (validateComposition) + //This mapping will cause a lot of content type validation to occur which we need to deal with + try { - //NOTE: this throws an error response if it is not valid - ValidateComposition(contentTypeSave, ct); + Mapper.Map(contentTypeSave, ct); } + catch (Exception ex) + { + var responseEx = CreateInvalidCompositionResponseException(ex, contentTypeSave, ct, ctId); + if (responseEx != null) throw responseEx; + } + + var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, ct); + if (exResult != null) throw exResult; saveContentType(ct); @@ -260,12 +216,7 @@ namespace Umbraco.Web.Editors { beforeCreateNew(contentTypeSave); } - - //set id to null to ensure its handled as a new type - contentTypeSave.Id = null; - contentTypeSave.CreateDate = DateTime.Now; - contentTypeSave.UpdateDate = DateTime.Now; - + //check if the type is trying to allow type 0 below itself - id zero refers to the currently unsaved type //always filter these 0 types out var allowItselfAsChild = false; @@ -276,13 +227,26 @@ namespace Umbraco.Web.Editors } //save as new - var newCt = Mapper.Map(contentTypeSave); - - if (validateComposition) + + TContentType newCt = null; + try { - //NOTE: this throws an error response if it is not valid - ValidateComposition(contentTypeSave, newCt); + //This mapping will cause a lot of content type validation to occur which we need to deal with + newCt = Mapper.Map(contentTypeSave); } + catch (Exception ex) + { + var responseEx = CreateInvalidCompositionResponseException(ex, contentTypeSave, ct, ctId); + if (responseEx != null) throw responseEx; + } + + var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, newCt); + if (exResult != null) throw exResult; + + //set id to null to ensure its handled as a new type + contentTypeSave.Id = null; + contentTypeSave.CreateDate = DateTime.Now; + contentTypeSave.UpdateDate = DateTime.Now; saveContentType(newCt); @@ -296,7 +260,7 @@ namespace Umbraco.Web.Editors return newCt; } } - + /// /// Change the sort order for media /// @@ -341,6 +305,121 @@ namespace Umbraco.Web.Editors } } + /// + /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors + /// + /// + /// + /// + private HttpResponseException CreateCompositionValidationExceptionIfInvalid(TContentTypeSave contentTypeSave, IContentTypeComposition composition) + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + where TContentTypeDisplay : ContentTypeCompositionDisplay + { + var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); + if (validateAttempt == false) + { + //if it's not successful then we need to return some model state for the property aliases that + // are duplicated + var invalidPropertyAliases = validateAttempt.Result.Distinct(); + AddCompositionValidationErrors(contentTypeSave, invalidPropertyAliases); + + var display = Mapper.Map(composition); + //map the 'save' data on top + display = Mapper.Map(contentTypeSave, display); + display.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + return null; + } + + /// + /// Adds errors to the model state if any invalid aliases are found then throws an error response if there are errors + /// + /// + /// + /// + private void AddCompositionValidationErrors(TContentTypeSave contentTypeSave, IEnumerable invalidPropertyAliases) + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + foreach (var propertyAlias in invalidPropertyAliases) + { + //find the property relating to these + var prop = contentTypeSave.Groups.SelectMany(x => x.Properties).Single(x => x.Alias == propertyAlias); + var group = contentTypeSave.Groups.Single(x => x.Properties.Contains(prop)); + + var key = string.Format("Groups[{0}].Properties[{1}].Alias", group.SortOrder, prop.SortOrder); + ModelState.AddModelError(key, "Duplicate property aliases not allowed between compositions"); + } + } + + /// + /// If the exception is an InvalidCompositionException create a response exception to be thrown for validation errors + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private HttpResponseException CreateInvalidCompositionResponseException( + Exception ex, TContentTypeSave contentTypeSave, TContentType ct, int ctId) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + InvalidCompositionException invalidCompositionException = null; + if (ex is AutoMapperMappingException && ex.InnerException is InvalidCompositionException) + { + invalidCompositionException = (InvalidCompositionException)ex.InnerException; + } + else if (ex.InnerException is InvalidCompositionException) + { + invalidCompositionException = (InvalidCompositionException)ex; + } + if (invalidCompositionException != null) + { + AddCompositionValidationErrors(contentTypeSave, invalidCompositionException.PropertyTypeAliases); + return CreateModelStateValidationException(ctId, contentTypeSave, ct); + } + return null; + } + + /// + /// Used to throw the ModelState validation results when the ModelState is invalid + /// + /// + /// + /// + /// + /// + private HttpResponseException CreateModelStateValidationException(int ctId, ContentTypeSave contentTypeSave, TContentType ct) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + { + TContentTypeDisplay forDisplay; + if (ctId > 0) + { + //Required data is invalid so we cannot continue + forDisplay = Mapper.Map(ct); + //map the 'save' data on top + forDisplay = Mapper.Map(contentTypeSave, forDisplay); + } + else + { + //map the 'save' data to display + forDisplay = Mapper.Map(contentTypeSave); + } + + forDisplay.Errors = ModelState.ToErrorDictionary(); + return new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } + private ICultureDictionary CultureDictionary { get @@ -351,5 +430,6 @@ namespace Umbraco.Web.Editors } } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 8e50727cd0..8ed61fd52c 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -134,8 +134,7 @@ namespace Umbraco.Web.Editors var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, getContentType: i => Services.MemberTypeService.Get(i), - saveContentType: type => Services.MemberTypeService.Save(type), - validateComposition: false); + saveContentType: type => Services.MemberTypeService.Save(type)); var display = Mapper.Map(savedCt); diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs index f79192caf6..5d961487ad 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs @@ -77,10 +77,10 @@ namespace Umbraco.Web.Models.ContentEditing if (duplicateGroups.Any()) { //we need to return the field name with an index so it's wired up correctly - var firstIndex = Groups.IndexOf(duplicateGroups.First().First()); + var lastIndex = Groups.IndexOf(duplicateGroups.Last().Last()); yield return new ValidationResult("Duplicate group names not allowed", new[] { - string.Format("Groups[{0}].Name", firstIndex) + string.Format("Groups[{0}].Name", lastIndex) }); } @@ -88,14 +88,12 @@ namespace Umbraco.Web.Models.ContentEditing if (duplicateProperties.Any()) { //we need to return the field name with an index so it's wired up correctly - var firstProperty = duplicateProperties.First().First(); - var propertyGroup = Groups.Single(x => x.Properties.Contains(firstProperty)); - var groupIndex = Groups.IndexOf(propertyGroup); - var propertyIndex = propertyGroup.Properties.IndexOf(firstProperty); + var lastProperty = duplicateProperties.Last().Last(); + var propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty)); yield return new ValidationResult("Duplicate property aliases not allowed", new[] { - string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex) + string.Format("Groups[{0}].Properties[{1}].Alias", propertyGroup.SortOrder, lastProperty.SortOrder) }); }