From 6555fd57917f11d33caa886c75d77286d6c5c77b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 3 Dec 2014 21:06:01 +1100 Subject: [PATCH] Updated U4-4049 - we now use media formatters instead of IModelBinder (this is actually the preferred way anyways). The validation is then moved to just a filter instead of done in the model binder. This way a media formatter is pure async, so we can ReadFromStreamAsync in a completely native async operation which should hopefuly fix this issue. --- ...terAllowedOutgoingContentAttributeTests.cs | 1 + src/Umbraco.Web/Editors/ContentController.cs | 8 +- .../Editors/ContentModelValidationFilter.cs | 106 +++++ src/Umbraco.Web/Editors/DataTypeController.cs | 1 - .../Filters}/ContentItemBaseBinder.cs | 378 ++++++++--------- .../Filters/ContentItemBaseFormatter.cs | 143 +++++++ .../Filters/ContentItemFormatter.cs} | 89 ++-- ...entModelFormatterConfigurationAttribute.cs | 29 ++ .../Filters/ContentValidationHelper.cs} | 318 +++++++------- .../FilterAllowedOutgoingContentAttribute.cs | 201 +++++---- .../FilterAllowedOutgoingMediaAttribute.cs | 3 +- .../Filters/MediaItemFormatter.cs} | 88 ++-- .../Filters/MemberFormatter.cs} | 394 ++++++++---------- .../Editors/Filters/MemberValidationHelper.cs | 27 ++ src/Umbraco.Web/Editors/MediaController.cs | 8 +- .../Editors/MediaPostValidateAttribute.cs | 1 + src/Umbraco.Web/Editors/MemberController.cs | 8 +- src/Umbraco.Web/Umbraco.Web.csproj | 18 +- 18 files changed, 1037 insertions(+), 784 deletions(-) create mode 100644 src/Umbraco.Web/Editors/ContentModelValidationFilter.cs rename src/Umbraco.Web/{WebApi/Binders => Editors/Filters}/ContentItemBaseBinder.cs (86%) create mode 100644 src/Umbraco.Web/Editors/Filters/ContentItemBaseFormatter.cs rename src/Umbraco.Web/{WebApi/Binders/ContentItemBinder.cs => Editors/Filters/ContentItemFormatter.cs} (71%) create mode 100644 src/Umbraco.Web/Editors/Filters/ContentModelFormatterConfigurationAttribute.cs rename src/Umbraco.Web/{WebApi/Filters/ContentItemValidationHelper.cs => Editors/Filters/ContentValidationHelper.cs} (87%) rename src/Umbraco.Web/{WebApi => Editors}/Filters/FilterAllowedOutgoingContentAttribute.cs (95%) rename src/Umbraco.Web/{WebApi => Editors}/Filters/FilterAllowedOutgoingMediaAttribute.cs (98%) rename src/Umbraco.Web/{WebApi/Binders/MediaItemBinder.cs => Editors/Filters/MediaItemFormatter.cs} (74%) rename src/Umbraco.Web/{WebApi/Binders/MemberBinder.cs => Editors/Filters/MemberFormatter.cs} (82%) create mode 100644 src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs diff --git a/src/Umbraco.Tests/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs b/src/Umbraco.Tests/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs index c0076f17fb..777055dd2d 100644 --- a/src/Umbraco.Tests/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs +++ b/src/Umbraco.Tests/Controllers/WebApiEditors/FilterAllowedOutgoingContentAttributeTests.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using Umbraco.Web.Editors.Filters; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.WebApi.Filters; diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index d52a65fcb3..63517b53cf 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -16,13 +16,13 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Publishing; using Umbraco.Core.Services; +using Umbraco.Web.Editors.Filters; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; using Umbraco.Web.Security; using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using umbraco; using Umbraco.Core.Models; @@ -43,6 +43,7 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] + [ContentModelFormatterConfiguration(typeof(ContentItemFormatter))] public class ContentController : ContentControllerBase { /// @@ -199,9 +200,8 @@ namespace Umbraco.Web.Editors /// [FileUploadCleanupFilter] [ContentPostValidate] - public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) + [ContentModelValidationFilter(typeof(ContentItemSave), typeof(IContent))] + public ContentItemDisplay PostSave(ContentItemSave contentItem) { //If we've reached here it means: // * Our model has been bound diff --git a/src/Umbraco.Web/Editors/ContentModelValidationFilter.cs b/src/Umbraco.Web/Editors/ContentModelValidationFilter.cs new file mode 100644 index 0000000000..69e8c32cba --- /dev/null +++ b/src/Umbraco.Web/Editors/ContentModelValidationFilter.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Editors.Filters; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Editors +{ + /// + /// This filter is used on all PostSave methods for Content, Media and Members. It's purpose is to instantiate a new instance of + /// ContentItemValidationHelper{TPersisted, TModelSave} which is used to validate the content properties of these entity types. + /// This filter is then executed after the model is bound but before the action is executed. + /// + internal sealed class ContentModelValidationFilter : ActionFilterAttribute + { + private readonly Type _customValidationHelperType; + private readonly Type _contentItemSaveType; + private readonly Type _contentPersistedType; + + private static readonly ConcurrentDictionary, ValidationHelperReflectedType> ReflectionCache = new ConcurrentDictionary, ValidationHelperReflectedType>(); + + /// + /// Constructor accepting a custom instance of a ContentItemValidationHelper{TPersisted, TModelSave} + /// + /// + public ContentModelValidationFilter(Type customValidationHelperType) + { + if (customValidationHelperType == null) throw new ArgumentNullException("customValidationHelperType"); + _customValidationHelperType = customValidationHelperType; + } + + /// + /// Constructor accepting the types required to create an instance of the desired ContentItemValidationHelper{TPersisted, TModelSave} + /// + /// + /// + public ContentModelValidationFilter(Type contentItemSaveType, Type contentPersistedType) + { + if (contentItemSaveType == null) + throw new ArgumentNullException("contentItemSaveType"); + if (contentPersistedType == null) + throw new ArgumentNullException("contentPersistedType"); + if (TypeHelper.IsTypeAssignableFrom(contentItemSaveType) == false) + throw new ArgumentException("Invalid base type", "contentItemSaveType"); + if (TypeHelper.IsTypeAssignableFrom(contentPersistedType) == false) + throw new ArgumentException("Invalid base type", "contentPersistedType"); + + _contentItemSaveType = contentItemSaveType; + _contentPersistedType = contentPersistedType; + } + + /// + /// Occurs before the action method is invoked. + /// + /// The action context. + public override void OnActionExecuting(HttpActionContext actionContext) + { + var contentItem = actionContext.ActionArguments["contentItem"]; + + //NOTE: This will be an instance of ContentItemValidationHelper + object validator; + MethodInfo validateMethod; + if (_customValidationHelperType != null) + { + //Get the validator for this generic type + validator = Activator.CreateInstance(_customValidationHelperType); + validateMethod = _customValidationHelperType.GetMethod("ValidateItem"); + } + else + { + var reflectedInfo = ReflectionCache.GetOrAdd(new Tuple(_contentItemSaveType, _contentPersistedType), + tuple => + { + var validationHelperGenericType = typeof(ContentValidationHelper<,>); + var realType = validationHelperGenericType.MakeGenericType(_contentPersistedType, _contentItemSaveType); + return new ValidationHelperReflectedType + { + RealType = realType, + ValidateMethod = realType.GetMethod("ValidateItem") + }; + }); + + //Get the validator for this generic type + validator = Activator.CreateInstance(reflectedInfo.RealType); + + validateMethod = reflectedInfo.ValidateMethod; + } + + + //Now call the methods for this instance + validateMethod.Invoke(validator, new object[] { actionContext, contentItem }); + + } + + private class ValidationHelperReflectedType + { + public Type RealType { get; set; } + public MethodInfo ValidateMethod { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 2b3c75838b..1d405eb0a3 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -14,7 +14,6 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using umbraco; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs b/src/Umbraco.Web/Editors/Filters/ContentItemBaseBinder.cs similarity index 86% rename from src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs rename to src/Umbraco.Web/Editors/Filters/ContentItemBaseBinder.cs index b5510ec85d..9804e310bf 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentItemBaseBinder.cs @@ -1,197 +1,183 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Formatting; -using System.Text; -using System.Threading.Tasks; -using System.Web; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding.Binders; -using System.Web.Http.Validation; -using System.Web.ModelBinding; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Models; -using Umbraco.Web.Editors; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; -using Umbraco.Web.WebApi.Filters; -using IModelBinder = System.Web.Http.ModelBinding.IModelBinder; -using ModelBindingContext = System.Web.Http.ModelBinding.ModelBindingContext; -using ModelMetadata = System.Web.Http.Metadata.ModelMetadata; -using ModelMetadataProvider = System.Web.Http.Metadata.ModelMetadataProvider; -using MutableObjectModelBinder = System.Web.Http.ModelBinding.Binders.MutableObjectModelBinder; -using Task = System.Threading.Tasks.Task; - -namespace Umbraco.Web.WebApi.Binders -{ - /// - /// Binds the content model to the controller action for the posted multi-part Post - /// - internal abstract class ContentItemBaseBinder : IModelBinder - where TPersisted : class, IContentBase - where TModelSave : ContentBaseItemSave - { - protected ApplicationContext ApplicationContext { get; private set; } - - /// - /// Constructor - /// - /// - internal ContentItemBaseBinder(ApplicationContext applicationContext) - { - ApplicationContext = applicationContext; - } - - public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) - { - //NOTE: Validation is done in the filter - if (actionContext.Request.Content.IsMimeMultipartContent() == false) - { - throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); - } - - var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); - //ensure it exists - Directory.CreateDirectory(root); - var provider = new MultipartFormDataStreamProvider(root); - - var task = Task.Run(() => GetModelAsync(actionContext, bindingContext, provider)) - .ContinueWith(x => - { - if (x.IsFaulted && x.Exception != null) - { - throw x.Exception; - } - - //now that everything is binded, validate the properties - var contentItemValidator = GetValidationHelper(); - contentItemValidator.ValidateItem(actionContext, x.Result); - - bindingContext.Model = x.Result; - }); - - task.Wait(); - - return bindingContext.Model != null; - } - - protected virtual ContentItemValidationHelper GetValidationHelper() - { - return new ContentItemValidationHelper(); - } - - /// - /// Builds the model from the request contents - /// - /// - /// - /// - /// - private async Task GetModelAsync(HttpActionContext actionContext, ModelBindingContext bindingContext, MultipartFormDataStreamProvider provider) - { - var request = actionContext.Request; - - //IMPORTANT!!! We need to ensure the umbraco context here because this is running in an async thread - var httpContext = (HttpContextBase) request.Properties["MS_HttpContext"]; - UmbracoContext.EnsureContext( - httpContext, - ApplicationContext.Current, - new WebSecurity(httpContext, ApplicationContext.Current)); - - var content = request.Content; - - var result = await content.ReadAsMultipartAsync(provider); - - if (result.FormData["contentItem"] == null) - { - var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest); - response.ReasonPhrase = "The request was not formatted correctly and is missing the 'contentItem' parameter"; - throw new HttpResponseException(response); - } - - //get the string json from the request - var contentItem = result.FormData["contentItem"]; - - //deserialize into our model - var model = JsonConvert.DeserializeObject(contentItem); - - //get the default body validator and validate the object - var bodyValidator = actionContext.ControllerContext.Configuration.Services.GetBodyModelValidator(); - var metadataProvider = actionContext.ControllerContext.Configuration.Services.GetModelMetadataProvider(); - //all validation errors will not contain a prefix - bodyValidator.Validate(model, typeof(TModelSave), metadataProvider, actionContext, ""); - - //get the files - foreach (var file in result.FileData) - { - //The name that has been assigned in JS has 2 parts and the second part indicates the property id - // for which the file belongs. - var parts = file.Headers.ContentDisposition.Name.Trim(new char[] { '\"' }).Split('_'); - if (parts.Length != 2) - { - var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest); - response.ReasonPhrase = "The request was not formatted correctly the file name's must be underscore delimited"; - throw new HttpResponseException(response); - } - var propAlias = parts[1]; - - var fileName = file.Headers.ContentDisposition.FileName.Trim(new char[] {'\"'}); - - model.UploadedFiles.Add(new ContentItemFile - { - TempFilePath = file.LocalFileName, - PropertyAlias = propAlias, - FileName = fileName - }); - } - - if (ContentControllerBase.IsCreatingAction(model.Action)) - { - //we are creating new content - model.PersistedContent = CreateNew(model); - } - else - { - //finally, let's lookup the real content item and create the DTO item - model.PersistedContent = GetExisting(model); - } - - //create the dto from the persisted model - if (model.PersistedContent != null) - { - model.ContentDto = MapFromPersisted(model); - } - if (model.ContentDto != null) - { - //now map all of the saved values to the dto - MapPropertyValuesFromSaved(model, model.ContentDto); - } - - return model; - } - - /// - /// we will now assign all of the values in the 'save' model to the DTO object - /// - /// - /// - private static void MapPropertyValuesFromSaved(TModelSave saveModel, ContentItemDto dto) - { - foreach (var p in saveModel.Properties.Where(p => dto.Properties.Any(x => x.Alias == p.Alias))) - { - dto.Properties.Single(x => x.Alias == p.Alias).Value = p.Value; - } - } - - protected abstract TPersisted GetExisting(TModelSave model); - protected abstract TPersisted CreateNew(TModelSave model); - protected abstract ContentItemDto MapFromPersisted(TModelSave model); - } +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; +using Task = System.Threading.Tasks.Task; + +namespace Umbraco.Web.Editors.Filters +{ + /// + /// Binds the content model to the controller action for the posted multi-part Post + /// + internal abstract class ContentItemBaseBinder : IModelBinder + where TPersisted : class, IContentBase + where TModelSave : ContentBaseItemSave + { + protected ApplicationContext ApplicationContext { get; private set; } + + /// + /// Constructor + /// + /// + internal ContentItemBaseBinder(ApplicationContext applicationContext) + { + ApplicationContext = applicationContext; + } + + public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) + { + //NOTE: Validation is done in the filter + if (actionContext.Request.Content.IsMimeMultipartContent() == false) + { + throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); + } + + var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); + //ensure it exists + Directory.CreateDirectory(root); + var provider = new MultipartFormDataStreamProvider(root); + + var task = Task.Run(() => GetModelAsync(actionContext, bindingContext, provider)) + .ContinueWith(x => + { + if (x.IsFaulted && x.Exception != null) + { + throw x.Exception; + } + + //now that everything is binded, validate the properties + var contentItemValidator = GetValidationHelper(); + contentItemValidator.ValidateItem(actionContext, x.Result); + + bindingContext.Model = x.Result; + }); + + task.Wait(); + + return bindingContext.Model != null; + } + + protected virtual ContentValidationHelper GetValidationHelper() + { + return new ContentValidationHelper(); + } + + /// + /// Builds the model from the request contents + /// + /// + /// + /// + /// + private async Task GetModelAsync(HttpActionContext actionContext, ModelBindingContext bindingContext, MultipartFormDataStreamProvider provider) + { + var request = actionContext.Request; + + //IMPORTANT!!! We need to ensure the umbraco context here because this is running in an async thread + var httpContext = (HttpContextBase) request.Properties["MS_HttpContext"]; + UmbracoContext.EnsureContext( + httpContext, + ApplicationContext.Current, + new WebSecurity(httpContext, ApplicationContext.Current)); + + var content = request.Content; + + var result = await content.ReadAsMultipartAsync(provider); + + if (result.FormData["contentItem"] == null) + { + var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest); + response.ReasonPhrase = "The request was not formatted correctly and is missing the 'contentItem' parameter"; + throw new HttpResponseException(response); + } + + //get the string json from the request + var contentItem = result.FormData["contentItem"]; + + //deserialize into our model + var model = JsonConvert.DeserializeObject(contentItem); + + //get the default body validator and validate the object + var bodyValidator = actionContext.ControllerContext.Configuration.Services.GetBodyModelValidator(); + var metadataProvider = actionContext.ControllerContext.Configuration.Services.GetModelMetadataProvider(); + //all validation errors will not contain a prefix + bodyValidator.Validate(model, typeof(TModelSave), metadataProvider, actionContext, ""); + + //get the files + foreach (var file in result.FileData) + { + //The name that has been assigned in JS has 2 parts and the second part indicates the property id + // for which the file belongs. + var parts = file.Headers.ContentDisposition.Name.Trim(new char[] { '\"' }).Split('_'); + if (parts.Length != 2) + { + var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest); + response.ReasonPhrase = "The request was not formatted correctly the file name's must be underscore delimited"; + throw new HttpResponseException(response); + } + var propAlias = parts[1]; + + var fileName = file.Headers.ContentDisposition.FileName.Trim(new char[] {'\"'}); + + model.UploadedFiles.Add(new ContentItemFile + { + TempFilePath = file.LocalFileName, + PropertyAlias = propAlias, + FileName = fileName + }); + } + + if (ContentControllerBase.IsCreatingAction(model.Action)) + { + //we are creating new content + model.PersistedContent = CreateNew(model); + } + else + { + //finally, let's lookup the real content item and create the DTO item + model.PersistedContent = GetExisting(model); + } + + //create the dto from the persisted model + if (model.PersistedContent != null) + { + model.ContentDto = MapFromPersisted(model); + } + if (model.ContentDto != null) + { + //now map all of the saved values to the dto + MapPropertyValuesFromSaved(model, model.ContentDto); + } + + return model; + } + + /// + /// we will now assign all of the values in the 'save' model to the DTO object + /// + /// + /// + private static void MapPropertyValuesFromSaved(TModelSave saveModel, ContentItemDto dto) + { + foreach (var p in saveModel.Properties.Where(p => dto.Properties.Any(x => x.Alias == p.Alias))) + { + dto.Properties.Single(x => x.Alias == p.Alias).Value = p.Value; + } + } + + protected abstract TPersisted GetExisting(TModelSave model); + protected abstract TPersisted CreateNew(TModelSave model); + protected abstract ContentItemDto MapFromPersisted(TModelSave model); + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/Filters/ContentItemBaseFormatter.cs b/src/Umbraco.Web/Editors/Filters/ContentItemBaseFormatter.cs new file mode 100644 index 0000000000..c2092a8901 --- /dev/null +++ b/src/Umbraco.Web/Editors/Filters/ContentItemBaseFormatter.cs @@ -0,0 +1,143 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Web.Http; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors.Filters +{ + /// + /// Binds the content model to the controller action for the posted multi-part Post + /// + internal abstract class ContentItemBaseFormatter : MediaTypeFormatter + where TPersisted : class, IContentBase + where TModelSave : ContentBaseItemSave + { + protected ApplicationContext ApplicationContext { get; private set; } + + public override bool CanReadType(Type type) + { + return (type == typeof(TModelSave)); + } + + public override bool CanWriteType(Type type) + { + return false; + } + + /// + /// Constructor + /// + /// + internal ContentItemBaseFormatter(ApplicationContext applicationContext) + { + ApplicationContext = applicationContext; + + this.SupportedMediaTypes.Clear(); + this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data")); + } + + /// + /// Asynchronously deserializes an object of the specified type. + /// + /// + /// A whose result will be the object instance that has been read. + /// + /// The type of object to deserialize.The to read.The for the content being read.The to log events to. + public override async Task ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger) + { + var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); + //ensure it exists + Directory.CreateDirectory(root); + var provider = new MultipartFormDataStreamProvider(root); + + var result = await content.ReadAsMultipartAsync(provider); + + if (result.FormData["contentItem"] == null) + { + const string errMsg = "The request was not formatted correctly and is missing the 'contentItem' parameter"; + formatterLogger.LogError(string.Empty, errMsg); + var response = new HttpResponseMessage(HttpStatusCode.BadRequest) + { + ReasonPhrase = errMsg + }; + throw new HttpResponseException(response); + } + + //get the string json from the request + var contentItem = result.FormData["contentItem"]; + + //deserialize into our model + var model = JsonConvert.DeserializeObject(contentItem); + + //get the files + foreach (var file in result.FileData) + { + //The name that has been assigned in JS has 2 parts and the second part indicates the property id + // for which the file belongs. + var parts = file.Headers.ContentDisposition.Name.Trim(new char[] { '\"' }).Split('_'); + if (parts.Length != 2) + { + formatterLogger.LogError(string.Empty, "The request was not formatted correctly the file name's must be underscore delimited"); + var response = new HttpResponseMessage(HttpStatusCode.BadRequest) + { + ReasonPhrase = "The request was not formatted correctly the file name's must be underscore delimited" + }; + throw new HttpResponseException(response); + } + var propAlias = parts[1]; + + var fileName = file.Headers.ContentDisposition.FileName.Trim(new char[] { '\"' }); + + model.UploadedFiles.Add(new ContentItemFile + { + TempFilePath = file.LocalFileName, + PropertyAlias = propAlias, + FileName = fileName + }); + } + + model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + + //create the dto from the persisted model + if (model.PersistedContent != null) + { + model.ContentDto = MapFromPersisted(model); + } + if (model.ContentDto != null) + { + //now map all of the saved values to the dto + MapPropertyValuesFromSaved(model, model.ContentDto); + } + + return model; + } + + /// + /// we will now assign all of the values in the 'save' model to the DTO object + /// + /// + /// + private static void MapPropertyValuesFromSaved(TModelSave saveModel, ContentItemDto dto) + { + foreach (var p in saveModel.Properties.Where(p => dto.Properties.Any(x => x.Alias == p.Alias))) + { + dto.Properties.Single(x => x.Alias == p.Alias).Value = p.Value; + } + } + + protected abstract TPersisted GetExisting(TModelSave model); + protected abstract TPersisted CreateNew(TModelSave model); + protected abstract ContentItemDto MapFromPersisted(TModelSave model); + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/Editors/Filters/ContentItemFormatter.cs similarity index 71% rename from src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs rename to src/Umbraco.Web/Editors/Filters/ContentItemFormatter.cs index 5afc7cee19..a8af195127 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentItemFormatter.cs @@ -1,46 +1,45 @@ -using System; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; - -namespace Umbraco.Web.WebApi.Binders -{ - internal class ContentItemBinder : ContentItemBaseBinder - { - - public ContentItemBinder(ApplicationContext applicationContext) - : base(applicationContext) - { - } - - /// - /// Constructor - /// - public ContentItemBinder() - : this(ApplicationContext.Current) - { - } - - protected override IContent GetExisting(ContentItemSave model) - { - return ApplicationContext.Services.ContentService.GetById(Convert.ToInt32(model.Id)); - } - - protected override IContent CreateNew(ContentItemSave model) - { - var contentType = ApplicationContext.Services.ContentTypeService.GetContentType(model.ContentTypeAlias); - if (contentType == null) - { - throw new InvalidOperationException("No content type found wth alias " + model.ContentTypeAlias); - } - return new Content(model.Name, model.ParentId, contentType); - } - - protected override ContentItemDto MapFromPersisted(ContentItemSave model) - { - return Mapper.Map>(model.PersistedContent); - } - } +using System; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors.Filters +{ + internal class ContentItemFormatter : ContentItemBaseFormatter + { + public ContentItemFormatter() + : this(Core.ApplicationContext.Current) + { + } + + /// + /// Constructor + /// + /// + public ContentItemFormatter(ApplicationContext applicationContext) + : base(applicationContext) + { + } + + protected override IContent GetExisting(ContentItemSave model) + { + return ApplicationContext.Services.ContentService.GetById(Convert.ToInt32(model.Id)); + } + + protected override IContent CreateNew(ContentItemSave model) + { + var contentType = ApplicationContext.Services.ContentTypeService.GetContentType(model.ContentTypeAlias); + if (contentType == null) + { + throw new InvalidOperationException("No content type found wth alias " + model.ContentTypeAlias); + } + return new Content(model.Name, model.ParentId, contentType); + } + + protected override ContentItemDto MapFromPersisted(ContentItemSave model) + { + return Mapper.Map>(model.PersistedContent); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/Filters/ContentModelFormatterConfigurationAttribute.cs b/src/Umbraco.Web/Editors/Filters/ContentModelFormatterConfigurationAttribute.cs new file mode 100644 index 0000000000..5e7e737309 --- /dev/null +++ b/src/Umbraco.Web/Editors/Filters/ContentModelFormatterConfigurationAttribute.cs @@ -0,0 +1,29 @@ +using System; +using System.Net.Http.Formatting; +using System.Web.Http.Controllers; +using Umbraco.Core; + +namespace Umbraco.Web.Editors.Filters +{ + /// + /// Used by the content, media, members controller to bind (format) the incoming Save model + /// + internal class ContentModelFormatterConfigurationAttribute : Attribute, IControllerConfiguration + { + private readonly Type _contentModelFormatterType; + + public ContentModelFormatterConfigurationAttribute(Type contentModelFormatterType) + { + if (contentModelFormatterType == null) throw new ArgumentNullException("contentModelFormatterType"); + if (TypeHelper.IsTypeAssignableFrom(contentModelFormatterType) == false) throw new ArgumentException("Invalid type allowed", "contentModelFormatterType"); + _contentModelFormatterType = contentModelFormatterType; + } + + public virtual void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + //add the multi-part formatter + controllerSettings.Formatters.Add((MediaTypeFormatter)Activator.CreateInstance(_contentModelFormatterType)); + } + } + +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs b/src/Umbraco.Web/Editors/Filters/ContentValidationHelper.cs similarity index 87% rename from src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs rename to src/Umbraco.Web/Editors/Filters/ContentValidationHelper.cs index 239b9bd576..81c5786307 100644 --- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentValidationHelper.cs @@ -1,167 +1,153 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// A validation helper class used with ContentItemValidationFilterAttribute to be shared between content, media, etc... - /// - /// - /// - /// - /// If any severe errors occur then the response gets set to an error and execution will not continue. Property validation - /// errors will just be added to the ModelState. - /// - internal class ContentItemValidationHelper - where TPersisted : class, IContentBase - where TModelSave : ContentBaseItemSave - { - - public void ValidateItem(HttpActionContext actionContext, string argumentName) - { - var contentItem = actionContext.ActionArguments[argumentName] as TModelSave; - if (contentItem == null) - { - actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "No " + typeof(TModelSave) + " found in request"); - return; - } - - ValidateItem(actionContext, contentItem); - - } - - public void ValidateItem(HttpActionContext actionContext, TModelSave contentItem) - { - //now do each validation step - if (ValidateExistingContent(contentItem, actionContext) == false) return; - if (ValidateProperties(contentItem, actionContext) == false) return; - if (ValidatePropertyData(contentItem, actionContext) == false) return; - } - - /// - /// Ensure the content exists - /// - /// - /// - /// - protected virtual bool ValidateExistingContent(ContentItemBasic postedItem, HttpActionContext actionContext) - { - if (postedItem.PersistedContent == null) - { - var message = string.Format("content with id: {0} was not found", postedItem.Id); - actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); - return false; - } - - return true; - } - - /// - /// Ensure all of the ids in the post are valid - /// - /// - /// - /// - protected virtual bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext) - { - return ValidateProperties(postedItem.Properties.ToArray(), postedItem.PersistedContent.Properties.ToArray(), actionContext); - } - - /// - /// This validates that all of the posted properties exist on the persisted entity - /// - /// - /// - /// - /// - protected bool ValidateProperties(ContentPropertyBasic[] postedProperties , Property[] persistedProperties, HttpActionContext actionContext) - { - foreach (var p in postedProperties) - { - if (persistedProperties.Any(property => property.Alias == p.Alias) == false) - { - //TODO: Do we return errors here ? If someone deletes a property whilst their editing then should we just - //save the property data that remains? Or inform them they need to reload... not sure. This problem exists currently too i think. - - var message = string.Format("property with alias: {0} was not found", p.Alias); - actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new InvalidOperationException(message)); - return false; - } - - } - return true; - } - - /// - /// Validates the data for each property - /// - /// - /// - /// - /// - /// All property data validation goes into the modelstate with a prefix of "Properties" - /// - protected virtual bool ValidatePropertyData(ContentItemBasic postedItem, HttpActionContext actionContext) - { - foreach (var p in postedItem.ContentDto.Properties) - { - var editor = p.PropertyEditor; - if (editor == null) - { - var message = string.Format("The property editor with alias: {0} was not found for property with id {1}", p.DataType.PropertyEditorAlias, p.Id); - LogHelper.Warn>(message); - //actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); - //return false; - continue; - } - - //get the posted value for this property - var postedValue = postedItem.Properties.Single(x => x.Alias == p.Alias).Value; - - //get the pre-values for this property - var preValues = p.PreValues; - - //TODO: when we figure out how to 'override' certain pre-value properties we'll either need to: - // * Combine the preValues with the overridden values stored with the document type property (but how to combine?) - // * Or, pass in the overridden values stored with the doc type property separately - - foreach (var result in editor.ValueEditor.Validators.SelectMany(v => v.Validate(postedValue, preValues, editor))) - { - actionContext.ModelState.AddPropertyError(result, p.Alias); - } - - //Now we need to validate the property based on the PropertyType validation (i.e. regex and required) - // NOTE: These will become legacy once we have pre-value overrides. - if (p.IsRequired) - { - foreach (var result in p.PropertyEditor.ValueEditor.RequiredValidator.Validate(postedValue, "", preValues, editor)) - { - actionContext.ModelState.AddPropertyError(result, p.Alias); - } - } - - if (p.ValidationRegExp.IsNullOrWhiteSpace() == false) - { - foreach (var result in p.PropertyEditor.ValueEditor.RegexValidator.Validate(postedValue, p.ValidationRegExp, preValues, editor)) - { - actionContext.ModelState.AddPropertyError(result, p.Alias); - } - } - } - - return actionContext.ModelState.IsValid; - } - - - } +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http.Controllers; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors.Filters +{ + /// + /// A validation helper class used with ContentItemValidationFilterAttribute to be shared between content, media, etc... + /// + /// + /// + /// + /// If any severe errors occur then the response gets set to an error and execution will not continue. Property validation + /// errors will just be added to the ModelState. + /// + internal class ContentValidationHelper + where TPersisted : class, IContentBase + where TModelSave : ContentBaseItemSave + { + + public void ValidateItem(HttpActionContext actionContext, TModelSave contentItem) + { + //now do each validation step + if (ValidateExistingContent(contentItem, actionContext) == false) return; + if (ValidateProperties(contentItem, actionContext) == false) return; + if (ValidatePropertyData(contentItem, actionContext) == false) return; + } + + /// + /// Ensure the content exists + /// + /// + /// + /// + protected virtual bool ValidateExistingContent(ContentItemBasic postedItem, HttpActionContext actionContext) + { + if (postedItem.PersistedContent == null) + { + var message = string.Format("content with id: {0} was not found", postedItem.Id); + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); + return false; + } + + return true; + } + + /// + /// Ensure all of the ids in the post are valid + /// + /// + /// + /// + protected virtual bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext) + { + return ValidateProperties(postedItem.Properties.ToArray(), postedItem.PersistedContent.Properties.ToArray(), actionContext); + } + + /// + /// This validates that all of the posted properties exist on the persisted entity + /// + /// + /// + /// + /// + protected bool ValidateProperties(ContentPropertyBasic[] postedProperties , Property[] persistedProperties, HttpActionContext actionContext) + { + foreach (var p in postedProperties) + { + if (persistedProperties.Any(property => property.Alias == p.Alias) == false) + { + //TODO: Do we return errors here ? If someone deletes a property whilst their editing then should we just + //save the property data that remains? Or inform them they need to reload... not sure. This problem exists currently too i think. + + var message = string.Format("property with alias: {0} was not found", p.Alias); + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new InvalidOperationException(message)); + return false; + } + + } + return true; + } + + /// + /// Validates the data for each property + /// + /// + /// + /// + /// + /// All property data validation goes into the modelstate with a prefix of "Properties" + /// + protected virtual bool ValidatePropertyData(ContentItemBasic postedItem, HttpActionContext actionContext) + { + foreach (var p in postedItem.ContentDto.Properties) + { + var editor = p.PropertyEditor; + if (editor == null) + { + var message = string.Format("The property editor with alias: {0} was not found for property with id {1}", p.DataType.PropertyEditorAlias, p.Id); + LogHelper.Warn>(message); + //actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); + //return false; + continue; + } + + //get the posted value for this property + var postedValue = postedItem.Properties.Single(x => x.Alias == p.Alias).Value; + + //get the pre-values for this property + var preValues = p.PreValues; + + //TODO: when we figure out how to 'override' certain pre-value properties we'll either need to: + // * Combine the preValues with the overridden values stored with the document type property (but how to combine?) + // * Or, pass in the overridden values stored with the doc type property separately + + foreach (var result in editor.ValueEditor.Validators.SelectMany(v => v.Validate(postedValue, preValues, editor))) + { + actionContext.ModelState.AddPropertyError(result, p.Alias); + } + + //Now we need to validate the property based on the PropertyType validation (i.e. regex and required) + // NOTE: These will become legacy once we have pre-value overrides. + if (p.IsRequired) + { + foreach (var result in p.PropertyEditor.ValueEditor.RequiredValidator.Validate(postedValue, "", preValues, editor)) + { + actionContext.ModelState.AddPropertyError(result, p.Alias); + } + } + + if (p.ValidationRegExp.IsNullOrWhiteSpace() == false) + { + foreach (var result in p.PropertyEditor.ValueEditor.RegexValidator.Validate(postedValue, p.ValidationRegExp, preValues, editor)) + { + actionContext.ModelState.AddPropertyError(result, p.Alias); + } + } + } + + return actionContext.ModelState.IsValid; + } + + + + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web/Editors/Filters/FilterAllowedOutgoingContentAttribute.cs similarity index 95% rename from src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs rename to src/Umbraco.Web/Editors/Filters/FilterAllowedOutgoingContentAttribute.cs index 3feb3b682a..1b32f65a5d 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -1,102 +1,101 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; -using umbraco.BusinessLogic.Actions; -using Umbraco.Core; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// This inspects the result of the action that returns a collection of content and removes - /// any item that the current user doesn't have access to - /// - internal sealed class FilterAllowedOutgoingContentAttribute : FilterAllowedOutgoingMediaAttribute - { - private readonly char _permissionToCheck; - - public FilterAllowedOutgoingContentAttribute(Type outgoingType) - : base(outgoingType) - { - _permissionToCheck = ActionBrowse.Instance.Letter; - } - - public FilterAllowedOutgoingContentAttribute(Type outgoingType, char permissionToCheck) - : base(outgoingType) - { - _permissionToCheck = permissionToCheck; - } - - public FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName) - : base(outgoingType, propertyName) - { - _permissionToCheck = ActionBrowse.Instance.Letter; - } - - protected override void FilterItems(IUser user, IList items) - { - base.FilterItems(user, items); - - FilterBasedOnPermissions(items, user, ApplicationContext.Current.Services.UserService); - } - - protected override int GetUserStartNode(IUser user) - { - return user.StartContentId; - } - - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinContent; } - } - - internal void FilterBasedOnPermissions(IList items, IUser user, IUserService userService) - { - var length = items.Count; - - if (length > 0) - { - var ids = new List(); - for (var i = 0; i < length; i++) - { - ids.Add(((dynamic)items[i]).Id); - } - //get all the permissions for these nodes in one call - var permissions = userService.GetPermissions(user, ids.ToArray()).ToArray(); - var toRemove = new List(); - foreach (dynamic item in items) - { - var nodePermission = permissions.Where(x => x.EntityId == Convert.ToInt32(item.Id)).ToArray(); - //if there are no permissions for this id then we need to check what the user's default - // permissions are. - if (nodePermission.Any() == false) - { - //var defaultP = user.DefaultPermissions - - toRemove.Add(item); - } - else - { - foreach (var n in nodePermission) - { - //if the permission being checked doesn't exist then remove the item - if (n.AssignedPermissions.Contains(_permissionToCheck.ToString(CultureInfo.InvariantCulture)) == false) - { - toRemove.Add(item); - } - } - } - } - foreach (var item in toRemove) - { - items.Remove(item); - } - } - } - - } +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Editors.Filters +{ + /// + /// This inspects the result of the action that returns a collection of content and removes + /// any item that the current user doesn't have access to + /// + internal sealed class FilterAllowedOutgoingContentAttribute : FilterAllowedOutgoingMediaAttribute + { + private readonly char _permissionToCheck; + + public FilterAllowedOutgoingContentAttribute(Type outgoingType) + : base(outgoingType) + { + _permissionToCheck = ActionBrowse.Instance.Letter; + } + + public FilterAllowedOutgoingContentAttribute(Type outgoingType, char permissionToCheck) + : base(outgoingType) + { + _permissionToCheck = permissionToCheck; + } + + public FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName) + : base(outgoingType, propertyName) + { + _permissionToCheck = ActionBrowse.Instance.Letter; + } + + protected override void FilterItems(IUser user, IList items) + { + base.FilterItems(user, items); + + FilterBasedOnPermissions(items, user, ApplicationContext.Current.Services.UserService); + } + + protected override int GetUserStartNode(IUser user) + { + return user.StartContentId; + } + + protected override int RecycleBinId + { + get { return Constants.System.RecycleBinContent; } + } + + internal void FilterBasedOnPermissions(IList items, IUser user, IUserService userService) + { + var length = items.Count; + + if (length > 0) + { + var ids = new List(); + for (var i = 0; i < length; i++) + { + ids.Add(((dynamic)items[i]).Id); + } + //get all the permissions for these nodes in one call + var permissions = userService.GetPermissions(user, ids.ToArray()).ToArray(); + var toRemove = new List(); + foreach (dynamic item in items) + { + var nodePermission = permissions.Where(x => x.EntityId == Convert.ToInt32(item.Id)).ToArray(); + //if there are no permissions for this id then we need to check what the user's default + // permissions are. + if (nodePermission.Any() == false) + { + //var defaultP = user.DefaultPermissions + + toRemove.Add(item); + } + else + { + foreach (var n in nodePermission) + { + //if the permission being checked doesn't exist then remove the item + if (n.AssignedPermissions.Contains(_permissionToCheck.ToString(CultureInfo.InvariantCulture)) == false) + { + toRemove.Add(item); + } + } + } + } + foreach (var item in toRemove) + { + items.Remove(item); + } + } + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/Editors/Filters/FilterAllowedOutgoingMediaAttribute.cs similarity index 98% rename from src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs rename to src/Umbraco.Web/Editors/Filters/FilterAllowedOutgoingMediaAttribute.cs index 36170627d9..5bd692b36a 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -7,9 +7,8 @@ using System.Web.Http.Filters; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; -using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Web.WebApi.Filters +namespace Umbraco.Web.Editors.Filters { /// /// This inspects the result of the action that returns a collection of content and removes diff --git a/src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs b/src/Umbraco.Web/Editors/Filters/MediaItemFormatter.cs similarity index 74% rename from src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs rename to src/Umbraco.Web/Editors/Filters/MediaItemFormatter.cs index 726994d43e..9245095632 100644 --- a/src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs +++ b/src/Umbraco.Web/Editors/Filters/MediaItemFormatter.cs @@ -1,45 +1,45 @@ -using System; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; - -namespace Umbraco.Web.WebApi.Binders -{ - internal class MediaItemBinder : ContentItemBaseBinder - { - public MediaItemBinder(ApplicationContext applicationContext) - : base(applicationContext) - { - } - - /// - /// Constructor - /// - public MediaItemBinder() - : this(ApplicationContext.Current) - { - } - - protected override IMedia GetExisting(MediaItemSave model) - { - return ApplicationContext.Services.MediaService.GetById(Convert.ToInt32(model.Id)); - } - - protected override IMedia CreateNew(MediaItemSave model) - { - var contentType = ApplicationContext.Services.ContentTypeService.GetMediaType(model.ContentTypeAlias); - if (contentType == null) - { - throw new InvalidOperationException("No content type found wth alias " + model.ContentTypeAlias); - } - return new Core.Models.Media(model.Name, model.ParentId, contentType); - } - - protected override ContentItemDto MapFromPersisted(MediaItemSave model) - { - return Mapper.Map>(model.PersistedContent); - } - } +using System; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors.Filters +{ + internal class MediaItemFormatter : ContentItemBaseFormatter + { + public MediaItemFormatter() + : this(Core.ApplicationContext.Current) + { + } + + /// + /// Constructor + /// + /// + public MediaItemFormatter(ApplicationContext applicationContext) + : base(applicationContext) + { + } + + protected override IMedia GetExisting(MediaItemSave model) + { + return ApplicationContext.Services.MediaService.GetById(Convert.ToInt32(model.Id)); + } + + protected override IMedia CreateNew(MediaItemSave model) + { + var contentType = ApplicationContext.Services.ContentTypeService.GetMediaType(model.ContentTypeAlias); + if (contentType == null) + { + throw new InvalidOperationException("No content type found wth alias " + model.ContentTypeAlias); + } + return new Core.Models.Media(model.Name, model.ParentId, contentType); + } + + protected override ContentItemDto MapFromPersisted(MediaItemSave model) + { + return Mapper.Map>(model.PersistedContent); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs b/src/Umbraco.Web/Editors/Filters/MemberFormatter.cs similarity index 82% rename from src/Umbraco.Web/WebApi/Binders/MemberBinder.cs rename to src/Umbraco.Web/Editors/Filters/MemberFormatter.cs index 3893167a73..1ef877381d 100644 --- a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs +++ b/src/Umbraco.Web/Editors/Filters/MemberFormatter.cs @@ -1,211 +1,185 @@ -using System; -using System.Collections.Generic; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; -using System.Web.Security; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.WebApi.Filters; -using System.Linq; -using Umbraco.Core.Models.Membership; - -namespace Umbraco.Web.WebApi.Binders -{ - internal class MemberBinder : ContentItemBaseBinder - { - public MemberBinder(ApplicationContext applicationContext) - : base(applicationContext) - { - } - - /// - /// Constructor - /// - public MemberBinder() - : this(ApplicationContext.Current) - { - } - - protected override ContentItemValidationHelper GetValidationHelper() - { - return new MemberValidationHelper(); - } - - /// - /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) - /// - /// - /// - protected override IMember GetExisting(MemberSave model) - { - var scenario = ApplicationContext.Services.MemberService.GetMembershipScenario(); - var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - switch (scenario) - { - case MembershipScenario.NativeUmbraco: - return GetExisting(model.Key); - case MembershipScenario.CustomProviderWithUmbracoLink: - case MembershipScenario.StandaloneCustomProvider: - default: - var membershipUser = provider.GetUser(model.Key, false); - if (membershipUser == null) - { - throw new InvalidOperationException("Could not find member with key " + model.Key); - } - - //TODO: Support this scenario! - //if (scenario == MembershipScenario.CustomProviderWithUmbracoLink) - //{ - // //if there's a 'Member' type then we should be able to just go get it from the db since it was created with a link - // // to our data. - // var memberType = ApplicationContext.Services.MemberTypeService.GetMemberType(Constants.Conventions.MemberTypes.Member); - // if (memberType != null) - // { - // var existing = GetExisting(model.Key); - // FilterContentTypeProperties(existing.ContentType, existing.ContentType.PropertyTypes.Select(x => x.Alias).ToArray()); - // } - //} - - //generate a member for a generic membership provider - //NOTE: We don't care about the password here, so just generate something - //var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N")); - - //var convertResult = membershipUser.ProviderUserKey.TryConvertTo(); - //if (convertResult.Success == false) - //{ - // throw new InvalidOperationException("Only membership providers that store a GUID as their ProviderUserKey are supported" + model.Key); - //} - //member.Key = convertResult.Result; - - var member = Mapper.Map(membershipUser); - - return member; - } - } - - private IMember GetExisting(Guid key) - { - var member = ApplicationContext.Services.MemberService.GetByKey(key); - if (member == null) - { - throw new InvalidOperationException("Could not find member with key " + key); - } - - var standardProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); - - //remove all membership properties, these values are set with the membership provider. - var exclude = standardProps.Select(x => x.Value.Alias).ToArray(); - - foreach (var remove in exclude) - { - member.Properties.Remove(remove); - } - return member; - } - - /// - /// Gets an instance of IMember used when creating a member - /// - /// - /// - /// - /// Depending on whether a custom membership provider is configured this will return different results. - /// - protected override IMember CreateNew(MemberSave model) - { - var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - - if (provider.IsUmbracoMembershipProvider()) - { - var contentType = ApplicationContext.Services.MemberTypeService.Get(model.ContentTypeAlias); - if (contentType == null) - { - throw new InvalidOperationException("No member type found wth alias " + model.ContentTypeAlias); - } - - //remove all membership properties, these values are set with the membership provider. - FilterMembershipProviderProperties(contentType); - - //return the new member with the details filled in - return new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, contentType); - } - else - { - //A custom membership provider is configured - - //NOTE: Below we are assigning the password to just a new GUID because we are not actually storing the password, however that - // field is mandatory in the database so we need to put something there. - - //If the default Member type exists, we'll use that to create the IMember - that way we can associate the custom membership - // provider to our data - eventually we can support editing custom properties with a custom provider. - var memberType = ApplicationContext.Services.MemberTypeService.Get(Constants.Conventions.MemberTypes.DefaultAlias); - if (memberType != null) - { - FilterContentTypeProperties(memberType, memberType.PropertyTypes.Select(x => x.Alias).ToArray()); - return new Member(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"), memberType); - } - - //generate a member for a generic membership provider - var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N")); - //we'll just remove all properties here otherwise we'll end up with validation errors, we don't want to persist any property data anyways - // in this case. - FilterContentTypeProperties(member.ContentType, member.ContentType.PropertyTypes.Select(x => x.Alias).ToArray()); - return member; - } - } - - /// - /// This will remove all of the special membership provider properties which were required to display the property editors - /// for editing - but the values have been mapped back ot the MemberSave object directly - we don't want to keep these properties - /// on the IMember because they will attempt to be persisted which we don't want since they might not even exist. - /// - /// - private void FilterMembershipProviderProperties(IContentTypeBase contentType) - { - var defaultProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); - //remove all membership properties, these values are set with the membership provider. - var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); - FilterContentTypeProperties(contentType, exclude); - } - - private void FilterContentTypeProperties(IContentTypeBase contentType, IEnumerable exclude) - { - //remove all properties based on the exclusion list - foreach (var remove in exclude) - { - if (contentType.PropertyTypeExists(remove)) - { - contentType.RemovePropertyType(remove); - } - } - } - - protected override ContentItemDto MapFromPersisted(MemberSave model) - { - return Mapper.Map>(model.PersistedContent); - } - - /// - /// Custom validation helper so that we can exclude the Member.StandardPropertyTypeStubs from being validating for existence - /// - internal class MemberValidationHelper : ContentItemValidationHelper - { - protected override bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext) - { - var propertiesToValidate = postedItem.Properties.ToList(); - var defaultProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); - var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); - foreach (var remove in exclude) - { - propertiesToValidate.RemoveAll(property => property.Alias == remove); - } - - return ValidateProperties(propertiesToValidate.ToArray(), postedItem.PersistedContent.Properties.ToArray(), actionContext); - } - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Security; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors.Filters +{ + internal class MemberFormatter : ContentItemBaseFormatter + { + public MemberFormatter() + : this(Core.ApplicationContext.Current) + { + } + + /// + /// Constructor + /// + /// + public MemberFormatter(ApplicationContext applicationContext) + : base(applicationContext) + { + } + + /// + /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) + /// + /// + /// + protected override IMember GetExisting(MemberSave model) + { + var scenario = ApplicationContext.Services.MemberService.GetMembershipScenario(); + var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + switch (scenario) + { + case MembershipScenario.NativeUmbraco: + return GetExisting(model.Key); + case MembershipScenario.CustomProviderWithUmbracoLink: + case MembershipScenario.StandaloneCustomProvider: + default: + var membershipUser = provider.GetUser(model.Key, false); + if (membershipUser == null) + { + throw new InvalidOperationException("Could not find member with key " + model.Key); + } + + //TODO: Support this scenario! + //if (scenario == MembershipScenario.CustomProviderWithUmbracoLink) + //{ + // //if there's a 'Member' type then we should be able to just go get it from the db since it was created with a link + // // to our data. + // var memberType = ApplicationContext.Services.MemberTypeService.GetMemberType(Constants.Conventions.MemberTypes.Member); + // if (memberType != null) + // { + // var existing = GetExisting(model.Key); + // FilterContentTypeProperties(existing.ContentType, existing.ContentType.PropertyTypes.Select(x => x.Alias).ToArray()); + // } + //} + + //generate a member for a generic membership provider + //NOTE: We don't care about the password here, so just generate something + //var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N")); + + //var convertResult = membershipUser.ProviderUserKey.TryConvertTo(); + //if (convertResult.Success == false) + //{ + // throw new InvalidOperationException("Only membership providers that store a GUID as their ProviderUserKey are supported" + model.Key); + //} + //member.Key = convertResult.Result; + + var member = Mapper.Map(membershipUser); + + return member; + } + } + + private IMember GetExisting(Guid key) + { + var member = ApplicationContext.Services.MemberService.GetByKey(key); + if (member == null) + { + throw new InvalidOperationException("Could not find member with key " + key); + } + + var standardProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + + //remove all membership properties, these values are set with the membership provider. + var exclude = standardProps.Select(x => x.Value.Alias).ToArray(); + + foreach (var remove in exclude) + { + member.Properties.Remove(remove); + } + return member; + } + + /// + /// Gets an instance of IMember used when creating a member + /// + /// + /// + /// + /// Depending on whether a custom membership provider is configured this will return different results. + /// + protected override IMember CreateNew(MemberSave model) + { + var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + + if (provider.IsUmbracoMembershipProvider()) + { + var contentType = ApplicationContext.Services.MemberTypeService.Get(model.ContentTypeAlias); + if (contentType == null) + { + throw new InvalidOperationException("No member type found wth alias " + model.ContentTypeAlias); + } + + //remove all membership properties, these values are set with the membership provider. + FilterMembershipProviderProperties(contentType); + + //return the new member with the details filled in + return new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, contentType); + } + else + { + //A custom membership provider is configured + + //NOTE: Below we are assigning the password to just a new GUID because we are not actually storing the password, however that + // field is mandatory in the database so we need to put something there. + + //If the default Member type exists, we'll use that to create the IMember - that way we can associate the custom membership + // provider to our data - eventually we can support editing custom properties with a custom provider. + var memberType = ApplicationContext.Services.MemberTypeService.Get(Constants.Conventions.MemberTypes.DefaultAlias); + if (memberType != null) + { + FilterContentTypeProperties(memberType, memberType.PropertyTypes.Select(x => x.Alias).ToArray()); + return new Member(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"), memberType); + } + + //generate a member for a generic membership provider + var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N")); + //we'll just remove all properties here otherwise we'll end up with validation errors, we don't want to persist any property data anyways + // in this case. + FilterContentTypeProperties(member.ContentType, member.ContentType.PropertyTypes.Select(x => x.Alias).ToArray()); + return member; + } + } + + /// + /// This will remove all of the special membership provider properties which were required to display the property editors + /// for editing - but the values have been mapped back ot the MemberSave object directly - we don't want to keep these properties + /// on the IMember because they will attempt to be persisted which we don't want since they might not even exist. + /// + /// + private void FilterMembershipProviderProperties(IContentTypeBase contentType) + { + var defaultProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + //remove all membership properties, these values are set with the membership provider. + var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); + FilterContentTypeProperties(contentType, exclude); + } + + private void FilterContentTypeProperties(IContentTypeBase contentType, IEnumerable exclude) + { + //remove all properties based on the exclusion list + foreach (var remove in exclude) + { + if (contentType.PropertyTypeExists(remove)) + { + contentType.RemovePropertyType(remove); + } + } + } + + protected override ContentItemDto MapFromPersisted(MemberSave model) + { + return Mapper.Map>(model.PersistedContent); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs b/src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs new file mode 100644 index 0000000000..b0a5229025 --- /dev/null +++ b/src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs @@ -0,0 +1,27 @@ +using System.Linq; +using System.Web.Http.Controllers; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors.Filters +{ + /// + /// Custom validation helper so that we can exclude the Member.StandardPropertyTypeStubs from being validating for existence + /// + internal class MemberValidationHelper : ContentValidationHelper + { + protected override bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext) + { + var propertiesToValidate = postedItem.Properties.ToList(); + var defaultProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); + foreach (var remove in exclude) + { + propertiesToValidate.RemoveAll(property => property.Alias == remove); + } + + return ValidateProperties(propertiesToValidate.ToArray(), postedItem.PersistedContent.Properties.ToArray(), actionContext); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 84c4128eb5..ae2bd0954b 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -20,13 +20,13 @@ using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Services; +using Umbraco.Web.Editors.Filters; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using System.Linq; -using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; @@ -41,6 +41,7 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Media)] + [ContentModelFormatterConfiguration(typeof(MediaItemFormatter))] public class MediaController : ContentControllerBase { /// @@ -214,9 +215,8 @@ namespace Umbraco.Web.Editors /// [FileUploadCleanupFilter] [MediaPostValidate] - public MediaItemDisplay PostSave( - [ModelBinder(typeof(MediaItemBinder))] - MediaItemSave contentItem) + [ContentModelValidationFilter(typeof(MediaItemSave), typeof(IMedia))] + public MediaItemDisplay PostSave(MediaItemSave contentItem) { //If we've reached here it means: // * Our model has been bound diff --git a/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs b/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs index ed26fcbc57..a65ecdb66d 100644 --- a/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs +++ b/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index a696ccf0b1..5ab4588941 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -21,11 +21,11 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Security; using Umbraco.Core.Services; +using Umbraco.Web.Editors.Filters; using Umbraco.Web.Models.Mapping; using Umbraco.Web.WebApi; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using umbraco; using Constants = Umbraco.Core.Constants; @@ -40,6 +40,7 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Members)] [OutgoingNoHyphenGuidFormat] + [ContentModelFormatterConfiguration(typeof(MemberFormatter))] public class MemberController : ContentControllerBase { /// @@ -231,9 +232,8 @@ namespace Umbraco.Web.Editors /// [FileUploadCleanupFilter] [MembershipProviderValidationFilter] - public MemberDisplay PostSave( - [ModelBinder(typeof(MemberBinder))] - MemberSave contentItem) + [ContentModelValidationFilter(typeof(MemberValidationHelper))] + public MemberDisplay PostSave(MemberSave contentItem) { //If we've reached here it means: diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 85bc54c778..8d4f936a06 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -268,6 +268,7 @@ + @@ -547,12 +548,16 @@ - + + + + + @@ -789,16 +794,14 @@ - + - - - + - - + + @@ -806,6 +809,7 @@ +