From 0120dc6ea30c73d5a90b8f562aa2151132d9dc3e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 7 Jun 2019 09:01:15 +0200 Subject: [PATCH] =?UTF-8?q?https://github.com/umbraco/Umbraco-CMS/issues/5?= =?UTF-8?q?574=20-=20Fixes=20issue=20when=E2=80=A6=20(#5575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Editors/ContentTypeControllerBase.cs | 1135 +++++++++-------- 1 file changed, 570 insertions(+), 565 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 5c75bc07f7..81dadae958 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -1,565 +1,570 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using System.Web.Http; -using AutoMapper; -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; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Constants = Umbraco.Core.Constants; - -namespace Umbraco.Web.Editors -{ - /// - /// Am abstract API controller providing functionality used for dealing with content and media types - /// - [PluginController("UmbracoApi")] - [PrefixlessBodyModelValidator] - public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController - { - private ICultureDictionary _cultureDictionary; - - /// - /// Constructor - /// - protected ContentTypeControllerBase() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - protected ContentTypeControllerBase(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - /// - /// Returns the available composite content types for a given content type - /// - /// - /// - /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out - /// along with any content types that have matching property types that are included in the filtered content types - /// - /// - /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. - /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot - /// be looked up via the db, they need to be passed in. - /// - /// - /// - protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, - UmbracoObjectTypes type, - string[] filterContentTypes, - string[] filterPropertyTypes) - { - IContentTypeComposition source = null; - - //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic - - IContentTypeComposition[] allContentTypes; - - switch (type) - { - case UmbracoObjectTypes.DocumentType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.GetContentType(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.ContentTypeService.GetAllContentTypes().Cast().ToArray(); - break; - - case UmbracoObjectTypes.MediaType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.GetMediaType(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.ContentTypeService.GetAllMediaTypes().Cast().ToArray(); - break; - - case UmbracoObjectTypes.MemberType: - if (contentTypeId > 0) - { - source = Services.MemberTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray(); - break; - - default: - throw new ArgumentOutOfRangeException("The entity type was not a content type"); - } - - var availableCompositions = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); - - Func> getEntityContainers = contentType => - { - if (contentType == null) - { - return null; - } - switch (type) - { - case UmbracoObjectTypes.DocumentType: - return Services.ContentTypeService.GetContentTypeContainers(contentType as IContentType); - case UmbracoObjectTypes.MediaType: - return Services.ContentTypeService.GetMediaTypeContainers(contentType as IMediaType); - case UmbracoObjectTypes.MemberType: - return new EntityContainer[0]; - default: - throw new ArgumentOutOfRangeException("The entity type was not a content type"); - } - }; - - var currCompositions = source == null ? new IContentTypeComposition[] { } : source.ContentTypeComposition.ToArray(); - var compAliases = currCompositions.Select(x => x.Alias).ToArray(); - var ancestors = availableCompositions.Ancestors.Select(x => x.Alias); - - return availableCompositions.Results - .Select(x => new Tuple(Mapper.Map(x.Composition), x.Allowed)) - .Select(x => - { - //we need to ensure that the item is enabled if it is already selected - // but do not allow it if it is any of the ancestors - if (compAliases.Contains(x.Item1.Alias) && ancestors.Contains(x.Item1.Alias) == false) - { - //re-set x to be allowed (NOTE: I didn't know you could set an enumerable item in a lambda!) - x = new Tuple(x.Item1, true); - } - - //translate the name - x.Item1.Name = TranslateItem(x.Item1.Name); - - var contentType = allContentTypes.FirstOrDefault(c => c.Key == x.Item1.Key); - var containers = getEntityContainers(contentType)?.ToArray(); - var containerPath = $"/{(containers != null && containers.Any() ? $"{string.Join("/", containers.Select(c => c.Name))}/" : null)}"; - x.Item1.AdditionalData["containerPath"] = containerPath; - - return x; - }) - .ToList(); - } - - /// - /// Returns a list of content types where a particular composition content type is used - /// - /// Type of content Type, eg documentType or mediaType - /// Id of composition content type - /// - protected IEnumerable PerformGetWhereCompositionIsUsedInContentTypes(int contentTypeId, - UmbracoObjectTypes type) - { - IContentTypeComposition source = null; - - //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic - - IContentTypeComposition[] allContentTypes; - - switch (type) - { - case UmbracoObjectTypes.DocumentType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.GetContentType(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.ContentTypeService.GetAllContentTypes().Cast().ToArray(); - break; - - case UmbracoObjectTypes.MediaType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.GetMediaType(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.ContentTypeService.GetAllMediaTypes().Cast().ToArray(); - break; - - case UmbracoObjectTypes.MemberType: - if (contentTypeId > 0) - { - source = Services.MemberTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray(); - break; - - default: - throw new ArgumentOutOfRangeException("The entity type was not a content type"); - } - - var contentTypesWhereCompositionIsUsed = Services.ContentTypeService.GetWhereCompositionIsUsedInContentTypes(source, allContentTypes); - return contentTypesWhereCompositionIsUsed - .Select(x => Mapper.Map(x)) - .Select(x => - { - //translate the name - x.Name = TranslateItem(x.Name); - - return x; - }) - .ToList(); - } - protected string TranslateItem(string text) - { - if (text == null) - { - return null; - } - - if (text.StartsWith("#") == false) - return text; - - text = text.Substring(1); - return CultureDictionary[text].IfNullOrWhiteSpace(text); - } - - protected TContentType PerformPostSave( - TContentTypeSave contentTypeSave, - Func getContentType, - Action saveContentType, - Action beforeCreateNew = null) - where TContentType : class, IContentTypeComposition - where TContentTypeDisplay : ContentTypeCompositionDisplay - where TContentTypeSave : ContentTypeSave - where TPropertyType : PropertyTypeBasic - { - var ctId = Convert.ToInt32(contentTypeSave.Id); - var ct = ctId > 0 ? getContentType(ctId) : null; - if (ctId > 0 && ct == null) throw new HttpResponseException(HttpStatusCode.NotFound); - - //Validate that there's no other ct with the same alias - // it in fact cannot be the same as any content type alias (member, content or media) because - // this would interfere with how ModelsBuilder works and also how many of the published caches - // works since that is based on aliases. - var allAliases = Services.ContentTypeService.GetAllContentTypeAliases(); - var exists = allAliases.InvariantContains(contentTypeSave.Alias); - if (exists && (ctId == 0 || ct.Alias.InvariantEquals(contentTypeSave.Alias) == false)) - { - ModelState.AddModelError("Alias", Services.TextService.Localize("editcontenttype/aliasAlreadyExists")); - } - - //now let the external validators execute - ValidationHelper.ValidateEditorModelWithResolver(ModelState, contentTypeSave); - - if (ModelState.IsValid == false) - { - throw CreateModelStateValidationException(ctId, contentTypeSave, ct); - } - - //filter out empty properties - contentTypeSave.Groups = contentTypeSave.Groups.Where(x => x.Name.IsNullOrWhiteSpace() == false).ToList(); - foreach (var group in contentTypeSave.Groups) - { - group.Properties = group.Properties.Where(x => x.Alias.IsNullOrWhiteSpace() == false).ToList(); - } - - if (ctId > 0) - { - //its an update to an existing content type - - //This mapping will cause a lot of content type validation to occur which we need to deal with - try - { - 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); - - return ct; - } - else - { - if (beforeCreateNew != null) - { - beforeCreateNew(contentTypeSave); - } - - //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; - if (contentTypeSave.AllowedContentTypes != null) - { - allowItselfAsChild = contentTypeSave.AllowedContentTypes.Any(x => x == 0); - contentTypeSave.AllowedContentTypes = contentTypeSave.AllowedContentTypes.Where(x => x > 0).ToList(); - } - - //save as new - - TContentType newCt = null; - try - { - //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); - - //we need to save it twice to allow itself under itself. - if (allowItselfAsChild) - { - //NOTE: This will throw if the composition isn't right... but it shouldn't be at this stage - newCt.AddContentType(newCt); - saveContentType(newCt); - } - return newCt; - } - } - - /// - /// Move - /// - /// - /// - /// - /// - protected HttpResponseMessage PerformMove( - MoveOrCopy move, - Func getContentType, - Func>> doMove) - where TContentType : IContentTypeComposition - { - var toMove = getContentType(move.Id); - if (toMove == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - var result = doMove(toMove, move.ParentId); - if (result.Success) - { - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - switch (result.Result.StatusType) - { - case MoveOperationStatusType.FailedParentNotFound: - return Request.CreateResponse(HttpStatusCode.NotFound); - case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - case MoveOperationStatusType.FailedNotAllowedByPath: - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return Request.CreateValidationErrorResponse(notificationModel); - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// Move - /// - /// - /// - /// - /// - protected HttpResponseMessage PerformCopy( - MoveOrCopy move, - Func getContentType, - Func>> doCopy) - where TContentType : IContentTypeComposition - { - var toMove = getContentType(move.Id); - if (toMove == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - var result = doCopy(toMove, move.ParentId); - if (result.Success) - { - var copy = result.Result.Entity; - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(copy.Path, Encoding.UTF8, "application/json"); - return response; - } - - switch (result.Result.StatusType) - { - case MoveOperationStatusType.FailedParentNotFound: - return Request.CreateResponse(HttpStatusCode.NotFound); - case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - case MoveOperationStatusType.FailedNotAllowedByPath: - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return Request.CreateValidationErrorResponse(notificationModel); - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// 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, TContentTypeSave contentTypeSave, TContentType ct) - where TContentType : class, IContentTypeComposition - where TContentTypeDisplay : ContentTypeCompositionDisplay - where TContentTypeSave : ContentTypeSave - { - 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 - { - return - _cultureDictionary ?? - (_cultureDictionary = CultureDictionaryFactoryResolver.Current.Factory.CreateDictionary()); - } - } - - - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Web.Http; +using AutoMapper; +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; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Editors +{ + /// + /// Am abstract API controller providing functionality used for dealing with content and media types + /// + [PluginController("UmbracoApi")] + [PrefixlessBodyModelValidator] + public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController + { + private ICultureDictionary _cultureDictionary; + + /// + /// Constructor + /// + protected ContentTypeControllerBase() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + protected ContentTypeControllerBase(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Returns the available composite content types for a given content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + /// + protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, + UmbracoObjectTypes type, + string[] filterContentTypes, + string[] filterPropertyTypes) + { + IContentTypeComposition source = null; + + //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic + + IContentTypeComposition[] allContentTypes; + + switch (type) + { + case UmbracoObjectTypes.DocumentType: + if (contentTypeId > 0) + { + source = Services.ContentTypeService.GetContentType(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.ContentTypeService.GetAllContentTypes().Cast().ToArray(); + break; + + case UmbracoObjectTypes.MediaType: + if (contentTypeId > 0) + { + source = Services.ContentTypeService.GetMediaType(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.ContentTypeService.GetAllMediaTypes().Cast().ToArray(); + break; + + case UmbracoObjectTypes.MemberType: + if (contentTypeId > 0) + { + source = Services.MemberTypeService.Get(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray(); + break; + + default: + throw new ArgumentOutOfRangeException("The entity type was not a content type"); + } + + var availableCompositions = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); + + Func> getEntityContainers = contentType => + { + if (contentType == null) + { + return null; + } + switch (type) + { + case UmbracoObjectTypes.DocumentType: + return Services.ContentTypeService.GetContentTypeContainers(contentType as IContentType); + case UmbracoObjectTypes.MediaType: + return Services.ContentTypeService.GetMediaTypeContainers(contentType as IMediaType); + case UmbracoObjectTypes.MemberType: + return new EntityContainer[0]; + default: + throw new ArgumentOutOfRangeException("The entity type was not a content type"); + } + }; + + var currCompositions = source == null ? new IContentTypeComposition[] { } : source.ContentTypeComposition.ToArray(); + var compAliases = currCompositions.Select(x => x.Alias).ToArray(); + var ancestors = availableCompositions.Ancestors.Select(x => x.Alias); + + return availableCompositions.Results + .Select(x => new Tuple(Mapper.Map(x.Composition), x.Allowed)) + .Select(x => + { + //we need to ensure that the item is enabled if it is already selected + // but do not allow it if it is any of the ancestors + if (compAliases.Contains(x.Item1.Alias) && ancestors.Contains(x.Item1.Alias) == false) + { + //re-set x to be allowed (NOTE: I didn't know you could set an enumerable item in a lambda!) + x = new Tuple(x.Item1, true); + } + + //translate the name + x.Item1.Name = TranslateItem(x.Item1.Name); + + var contentType = allContentTypes.FirstOrDefault(c => c.Key == x.Item1.Key); + var containers = getEntityContainers(contentType)?.ToArray(); + var containerPath = $"/{(containers != null && containers.Any() ? $"{string.Join("/", containers.Select(c => c.Name))}/" : null)}"; + x.Item1.AdditionalData["containerPath"] = containerPath; + + return x; + }) + .ToList(); + } + + /// + /// Returns a list of content types where a particular composition content type is used + /// + /// Type of content Type, eg documentType or mediaType + /// Id of composition content type + /// + protected IEnumerable PerformGetWhereCompositionIsUsedInContentTypes(int contentTypeId, + UmbracoObjectTypes type) + { + IContentTypeComposition source = null; + + //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic + + IContentTypeComposition[] allContentTypes; + + switch (type) + { + case UmbracoObjectTypes.DocumentType: + if (contentTypeId > 0) + { + source = Services.ContentTypeService.GetContentType(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.ContentTypeService.GetAllContentTypes().Cast().ToArray(); + break; + + case UmbracoObjectTypes.MediaType: + if (contentTypeId > 0) + { + source = Services.ContentTypeService.GetMediaType(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.ContentTypeService.GetAllMediaTypes().Cast().ToArray(); + break; + + case UmbracoObjectTypes.MemberType: + if (contentTypeId > 0) + { + source = Services.MemberTypeService.Get(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray(); + break; + + default: + throw new ArgumentOutOfRangeException("The entity type was not a content type"); + } + + var contentTypesWhereCompositionIsUsed = Services.ContentTypeService.GetWhereCompositionIsUsedInContentTypes(source, allContentTypes); + return contentTypesWhereCompositionIsUsed + .Select(x => Mapper.Map(x)) + .Select(x => + { + //translate the name + x.Name = TranslateItem(x.Name); + + return x; + }) + .ToList(); + } + protected string TranslateItem(string text) + { + if (text == null) + { + return null; + } + + if (text.StartsWith("#") == false) + return text; + + text = text.Substring(1); + return CultureDictionary[text].IfNullOrWhiteSpace(text); + } + + protected TContentType PerformPostSave( + TContentTypeSave contentTypeSave, + Func getContentType, + Action saveContentType, + Action beforeCreateNew = null) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + var ctId = Convert.ToInt32(contentTypeSave.Id); + var ct = ctId > 0 ? getContentType(ctId) : null; + if (ctId > 0 && ct == null) throw new HttpResponseException(HttpStatusCode.NotFound); + + //Validate that there's no other ct with the same alias + // it in fact cannot be the same as any content type alias (member, content or media) because + // this would interfere with how ModelsBuilder works and also how many of the published caches + // works since that is based on aliases. + var allAliases = Services.ContentTypeService.GetAllContentTypeAliases(); + var exists = allAliases.InvariantContains(contentTypeSave.Alias); + if (exists && (ctId == 0 || ct.Alias.InvariantEquals(contentTypeSave.Alias) == false)) + { + ModelState.AddModelError("Alias", Services.TextService.Localize("editcontenttype/aliasAlreadyExists")); + } + + //now let the external validators execute + ValidationHelper.ValidateEditorModelWithResolver(ModelState, contentTypeSave); + + if (ModelState.IsValid == false) + { + throw CreateModelStateValidationException(ctId, contentTypeSave, ct); + } + + //filter out empty properties + contentTypeSave.Groups = contentTypeSave.Groups.Where(x => x.Name.IsNullOrWhiteSpace() == false).ToList(); + foreach (var group in contentTypeSave.Groups) + { + group.Properties = group.Properties.Where(x => x.Alias.IsNullOrWhiteSpace() == false).ToList(); + } + + if (ctId > 0) + { + //its an update to an existing content type + + //This mapping will cause a lot of content type validation to occur which we need to deal with + try + { + 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); + + return ct; + } + else + { + if (beforeCreateNew != null) + { + beforeCreateNew(contentTypeSave); + } + + //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; + var allowIfselfAsChildSortOrder = -1; + if (contentTypeSave.AllowedContentTypes != null) + { + allowIfselfAsChildSortOrder = contentTypeSave.AllowedContentTypes.IndexOf(0); + allowItselfAsChild = contentTypeSave.AllowedContentTypes.Any(x => x == 0); + + contentTypeSave.AllowedContentTypes = contentTypeSave.AllowedContentTypes.Where(x => x > 0).ToList(); + } + + //save as new + + TContentType newCt = null; + try + { + //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); + + //we need to save it twice to allow itself under itself. + if (allowItselfAsChild && newCt != null) + { + newCt.AllowedContentTypes = + newCt.AllowedContentTypes.Union( + new []{ new ContentTypeSort(newCt.Id, allowIfselfAsChildSortOrder) } + ); + saveContentType(newCt); + } + return newCt; + } + } + + /// + /// Move + /// + /// + /// + /// + /// + protected HttpResponseMessage PerformMove( + MoveOrCopy move, + Func getContentType, + Func>> doMove) + where TContentType : IContentTypeComposition + { + var toMove = getContentType(move.Id); + if (toMove == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + var result = doMove(toMove, move.ParentId); + if (result.Success) + { + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + switch (result.Result.StatusType) + { + case MoveOperationStatusType.FailedParentNotFound: + return Request.CreateResponse(HttpStatusCode.NotFound); + case MoveOperationStatusType.FailedCancelledByEvent: + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + case MoveOperationStatusType.FailedNotAllowedByPath: + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + return Request.CreateValidationErrorResponse(notificationModel); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Move + /// + /// + /// + /// + /// + protected HttpResponseMessage PerformCopy( + MoveOrCopy move, + Func getContentType, + Func>> doCopy) + where TContentType : IContentTypeComposition + { + var toMove = getContentType(move.Id); + if (toMove == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + var result = doCopy(toMove, move.ParentId); + if (result.Success) + { + var copy = result.Result.Entity; + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(copy.Path, Encoding.UTF8, "application/json"); + return response; + } + + switch (result.Result.StatusType) + { + case MoveOperationStatusType.FailedParentNotFound: + return Request.CreateResponse(HttpStatusCode.NotFound); + case MoveOperationStatusType.FailedCancelledByEvent: + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + case MoveOperationStatusType.FailedNotAllowedByPath: + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + return Request.CreateValidationErrorResponse(notificationModel); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// 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, TContentTypeSave contentTypeSave, TContentType ct) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + { + 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 + { + return + _cultureDictionary ?? + (_cultureDictionary = CultureDictionaryFactoryResolver.Current.Factory.CreateDictionary()); + } + } + + + } +}