Merge branch '7.2.0-U4-4049' into dev-v7
Conflicts: src/Umbraco.Web/Umbraco.Web.csproj
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
/// </remarks>
|
||||
[PluginController("UmbracoApi")]
|
||||
[UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)]
|
||||
[ContentModelFormatterConfiguration(typeof(ContentItemFormatter))]
|
||||
public class ContentController : ContentControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -199,9 +200,8 @@ namespace Umbraco.Web.Editors
|
||||
/// <returns></returns>
|
||||
[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
|
||||
|
||||
106
src/Umbraco.Web/Editors/ContentModelValidationFilter.cs
Normal file
106
src/Umbraco.Web/Editors/ContentModelValidationFilter.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class ContentModelValidationFilter : ActionFilterAttribute
|
||||
{
|
||||
private readonly Type _customValidationHelperType;
|
||||
private readonly Type _contentItemSaveType;
|
||||
private readonly Type _contentPersistedType;
|
||||
|
||||
private static readonly ConcurrentDictionary<Tuple<Type, Type>, ValidationHelperReflectedType> ReflectionCache = new ConcurrentDictionary<Tuple<Type, Type>, ValidationHelperReflectedType>();
|
||||
|
||||
/// <summary>
|
||||
/// Constructor accepting a custom instance of a ContentItemValidationHelper{TPersisted, TModelSave}
|
||||
/// </summary>
|
||||
/// <param name="customValidationHelperType"></param>
|
||||
public ContentModelValidationFilter(Type customValidationHelperType)
|
||||
{
|
||||
if (customValidationHelperType == null) throw new ArgumentNullException("customValidationHelperType");
|
||||
_customValidationHelperType = customValidationHelperType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor accepting the types required to create an instance of the desired ContentItemValidationHelper{TPersisted, TModelSave}
|
||||
/// </summary>
|
||||
/// <param name="contentItemSaveType"></param>
|
||||
/// <param name="contentPersistedType"></param>
|
||||
public ContentModelValidationFilter(Type contentItemSaveType, Type contentPersistedType)
|
||||
{
|
||||
if (contentItemSaveType == null)
|
||||
throw new ArgumentNullException("contentItemSaveType");
|
||||
if (contentPersistedType == null)
|
||||
throw new ArgumentNullException("contentPersistedType");
|
||||
if (TypeHelper.IsTypeAssignableFrom<ContentItemBasic>(contentItemSaveType) == false)
|
||||
throw new ArgumentException("Invalid base type", "contentItemSaveType");
|
||||
if (TypeHelper.IsTypeAssignableFrom<IContentBase>(contentPersistedType) == false)
|
||||
throw new ArgumentException("Invalid base type", "contentPersistedType");
|
||||
|
||||
_contentItemSaveType = contentItemSaveType;
|
||||
_contentPersistedType = contentPersistedType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before the action method is invoked.
|
||||
/// </summary>
|
||||
/// <param name="actionContext">The action context.</param>
|
||||
public override void OnActionExecuting(HttpActionContext actionContext)
|
||||
{
|
||||
var contentItem = actionContext.ActionArguments["contentItem"];
|
||||
|
||||
//NOTE: This will be an instance of ContentItemValidationHelper<TPersisted, TModelSave>
|
||||
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<Type, Type>(_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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds the content model to the controller action for the posted multi-part Post
|
||||
/// </summary>
|
||||
internal abstract class ContentItemBaseBinder<TPersisted, TModelSave> : IModelBinder
|
||||
where TPersisted : class, IContentBase
|
||||
where TModelSave : ContentBaseItemSave<TPersisted>
|
||||
{
|
||||
protected ApplicationContext ApplicationContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="applicationContext"></param>
|
||||
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<TPersisted, TModelSave> GetValidationHelper()
|
||||
{
|
||||
return new ContentItemValidationHelper<TPersisted, TModelSave>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the model from the request contents
|
||||
/// </summary>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <param name="bindingContext"></param>
|
||||
/// <param name="provider"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<TModelSave> 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<TModelSave>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// we will now assign all of the values in the 'save' model to the DTO object
|
||||
/// </summary>
|
||||
/// <param name="saveModel"></param>
|
||||
/// <param name="dto"></param>
|
||||
private static void MapPropertyValuesFromSaved(TModelSave saveModel, ContentItemDto<TPersisted> 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<TPersisted> 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds the content model to the controller action for the posted multi-part Post
|
||||
/// </summary>
|
||||
internal abstract class ContentItemBaseBinder<TPersisted, TModelSave> : IModelBinder
|
||||
where TPersisted : class, IContentBase
|
||||
where TModelSave : ContentBaseItemSave<TPersisted>
|
||||
{
|
||||
protected ApplicationContext ApplicationContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="applicationContext"></param>
|
||||
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<TPersisted, TModelSave> GetValidationHelper()
|
||||
{
|
||||
return new ContentValidationHelper<TPersisted, TModelSave>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the model from the request contents
|
||||
/// </summary>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <param name="bindingContext"></param>
|
||||
/// <param name="provider"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<TModelSave> 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<TModelSave>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// we will now assign all of the values in the 'save' model to the DTO object
|
||||
/// </summary>
|
||||
/// <param name="saveModel"></param>
|
||||
/// <param name="dto"></param>
|
||||
private static void MapPropertyValuesFromSaved(TModelSave saveModel, ContentItemDto<TPersisted> 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<TPersisted> MapFromPersisted(TModelSave model);
|
||||
}
|
||||
}
|
||||
143
src/Umbraco.Web/Editors/Filters/ContentItemBaseFormatter.cs
Normal file
143
src/Umbraco.Web/Editors/Filters/ContentItemBaseFormatter.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds the content model to the controller action for the posted multi-part Post
|
||||
/// </summary>
|
||||
internal abstract class ContentItemBaseFormatter<TPersisted, TModelSave> : MediaTypeFormatter
|
||||
where TPersisted : class, IContentBase
|
||||
where TModelSave : ContentBaseItemSave<TPersisted>
|
||||
{
|
||||
protected ApplicationContext ApplicationContext { get; private set; }
|
||||
|
||||
public override bool CanReadType(Type type)
|
||||
{
|
||||
return (type == typeof(TModelSave));
|
||||
}
|
||||
|
||||
public override bool CanWriteType(Type type)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="applicationContext"></param>
|
||||
internal ContentItemBaseFormatter(ApplicationContext applicationContext)
|
||||
{
|
||||
ApplicationContext = applicationContext;
|
||||
|
||||
this.SupportedMediaTypes.Clear();
|
||||
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously deserializes an object of the specified type.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="T:System.Threading.Tasks.Task"/> whose result will be the object instance that has been read.
|
||||
/// </returns>
|
||||
/// <param name="type">The type of object to deserialize.</param><param name="readStream">The <see cref="T:System.IO.Stream"/> to read.</param><param name="content">The <see cref="T:System.Net.Http.HttpContent"/> for the content being read.</param><param name="formatterLogger">The <see cref="T:System.Net.Http.Formatting.IFormatterLogger"/> to log events to.</param>
|
||||
public override async Task<object> 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<TModelSave>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// we will now assign all of the values in the 'save' model to the DTO object
|
||||
/// </summary>
|
||||
/// <param name="saveModel"></param>
|
||||
/// <param name="dto"></param>
|
||||
private static void MapPropertyValuesFromSaved(TModelSave saveModel, ContentItemDto<TPersisted> 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<TPersisted> MapFromPersisted(TModelSave model);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<IContent, ContentItemSave>
|
||||
{
|
||||
|
||||
public ContentItemBinder(ApplicationContext applicationContext)
|
||||
: base(applicationContext)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
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<IContent> MapFromPersisted(ContentItemSave model)
|
||||
{
|
||||
return Mapper.Map<IContent, ContentItemDto<IContent>>(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<IContent, ContentItemSave>
|
||||
{
|
||||
public ContentItemFormatter()
|
||||
: this(Core.ApplicationContext.Current)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="applicationContext"></param>
|
||||
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<IContent> MapFromPersisted(ContentItemSave model)
|
||||
{
|
||||
return Mapper.Map<IContent, ContentItemDto<IContent>>(model.PersistedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Net.Http.Formatting;
|
||||
using System.Web.Http.Controllers;
|
||||
using Umbraco.Core;
|
||||
|
||||
namespace Umbraco.Web.Editors.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Used by the content, media, members controller to bind (format) the incoming Save model
|
||||
/// </summary>
|
||||
internal class ContentModelFormatterConfigurationAttribute : Attribute, IControllerConfiguration
|
||||
{
|
||||
private readonly Type _contentModelFormatterType;
|
||||
|
||||
public ContentModelFormatterConfigurationAttribute(Type contentModelFormatterType)
|
||||
{
|
||||
if (contentModelFormatterType == null) throw new ArgumentNullException("contentModelFormatterType");
|
||||
if (TypeHelper.IsTypeAssignableFrom<MediaTypeFormatter>(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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A validation helper class used with ContentItemValidationFilterAttribute to be shared between content, media, etc...
|
||||
/// </summary>
|
||||
/// <typeparam name="TPersisted"></typeparam>
|
||||
/// <typeparam name="TModelSave"></typeparam>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
internal class ContentItemValidationHelper<TPersisted, TModelSave>
|
||||
where TPersisted : class, IContentBase
|
||||
where TModelSave : ContentBaseItemSave<TPersisted>
|
||||
{
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure the content exists
|
||||
/// </summary>
|
||||
/// <param name="postedItem"></param>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual bool ValidateExistingContent(ContentItemBasic<ContentPropertyBasic, TPersisted> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure all of the ids in the post are valid
|
||||
/// </summary>
|
||||
/// <param name="postedItem"></param>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual bool ValidateProperties(ContentItemBasic<ContentPropertyBasic, TPersisted> postedItem, HttpActionContext actionContext)
|
||||
{
|
||||
return ValidateProperties(postedItem.Properties.ToArray(), postedItem.PersistedContent.Properties.ToArray(), actionContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This validates that all of the posted properties exist on the persisted entity
|
||||
/// </summary>
|
||||
/// <param name="postedProperties"></param>
|
||||
/// <param name="persistedProperties"></param>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the data for each property
|
||||
/// </summary>
|
||||
/// <param name="postedItem"></param>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// All property data validation goes into the modelstate with a prefix of "Properties"
|
||||
/// </remarks>
|
||||
protected virtual bool ValidatePropertyData(ContentItemBasic<ContentPropertyBasic, TPersisted> 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<ContentItemValidationHelper<TPersisted, TModelSave>>(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
|
||||
{
|
||||
/// <summary>
|
||||
/// A validation helper class used with ContentItemValidationFilterAttribute to be shared between content, media, etc...
|
||||
/// </summary>
|
||||
/// <typeparam name="TPersisted"></typeparam>
|
||||
/// <typeparam name="TModelSave"></typeparam>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
internal class ContentValidationHelper<TPersisted, TModelSave>
|
||||
where TPersisted : class, IContentBase
|
||||
where TModelSave : ContentBaseItemSave<TPersisted>
|
||||
{
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure the content exists
|
||||
/// </summary>
|
||||
/// <param name="postedItem"></param>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual bool ValidateExistingContent(ContentItemBasic<ContentPropertyBasic, TPersisted> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure all of the ids in the post are valid
|
||||
/// </summary>
|
||||
/// <param name="postedItem"></param>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual bool ValidateProperties(ContentItemBasic<ContentPropertyBasic, TPersisted> postedItem, HttpActionContext actionContext)
|
||||
{
|
||||
return ValidateProperties(postedItem.Properties.ToArray(), postedItem.PersistedContent.Properties.ToArray(), actionContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This validates that all of the posted properties exist on the persisted entity
|
||||
/// </summary>
|
||||
/// <param name="postedProperties"></param>
|
||||
/// <param name="persistedProperties"></param>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the data for each property
|
||||
/// </summary>
|
||||
/// <param name="postedItem"></param>
|
||||
/// <param name="actionContext"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// All property data validation goes into the modelstate with a prefix of "Properties"
|
||||
/// </remarks>
|
||||
protected virtual bool ValidatePropertyData(ContentItemBasic<ContentPropertyBasic, TPersisted> 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<ContentValidationHelper<TPersisted, TModelSave>>(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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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<int>();
|
||||
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<dynamic>();
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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<int>();
|
||||
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<dynamic>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This inspects the result of the action that returns a collection of content and removes
|
||||
@@ -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<IMedia, MediaItemSave>
|
||||
{
|
||||
public MediaItemBinder(ApplicationContext applicationContext)
|
||||
: base(applicationContext)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
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<IMedia> MapFromPersisted(MediaItemSave model)
|
||||
{
|
||||
return Mapper.Map<IMedia, ContentItemDto<IMedia>>(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<IMedia, MediaItemSave>
|
||||
{
|
||||
public MediaItemFormatter()
|
||||
: this(Core.ApplicationContext.Current)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="applicationContext"></param>
|
||||
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<IMedia> MapFromPersisted(MediaItemSave model)
|
||||
{
|
||||
return Mapper.Map<IMedia, ContentItemDto<IMedia>>(model.PersistedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IMember, MemberSave>
|
||||
{
|
||||
public MemberBinder(ApplicationContext applicationContext)
|
||||
: base(applicationContext)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public MemberBinder()
|
||||
: this(ApplicationContext.Current)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ContentItemValidationHelper<IMember, MemberSave> GetValidationHelper()
|
||||
{
|
||||
return new MemberValidationHelper();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an IMember instance used to bind values to and save (depending on the membership scenario)
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
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<Guid>();
|
||||
//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, IMember>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an instance of IMember used when creating a member
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// Depending on whether a custom membership provider is configured this will return different results.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="contentType"></param>
|
||||
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<string> exclude)
|
||||
{
|
||||
//remove all properties based on the exclusion list
|
||||
foreach (var remove in exclude)
|
||||
{
|
||||
if (contentType.PropertyTypeExists(remove))
|
||||
{
|
||||
contentType.RemovePropertyType(remove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override ContentItemDto<IMember> MapFromPersisted(MemberSave model)
|
||||
{
|
||||
return Mapper.Map<IMember, ContentItemDto<IMember>>(model.PersistedContent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom validation helper so that we can exclude the Member.StandardPropertyTypeStubs from being validating for existence
|
||||
/// </summary>
|
||||
internal class MemberValidationHelper : ContentItemValidationHelper<IMember, MemberSave>
|
||||
{
|
||||
protected override bool ValidateProperties(ContentItemBasic<ContentPropertyBasic, IMember> 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<IMember, MemberSave>
|
||||
{
|
||||
public MemberFormatter()
|
||||
: this(Core.ApplicationContext.Current)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="applicationContext"></param>
|
||||
public MemberFormatter(ApplicationContext applicationContext)
|
||||
: base(applicationContext)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an IMember instance used to bind values to and save (depending on the membership scenario)
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
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<Guid>();
|
||||
//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, IMember>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an instance of IMember used when creating a member
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// Depending on whether a custom membership provider is configured this will return different results.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="contentType"></param>
|
||||
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<string> exclude)
|
||||
{
|
||||
//remove all properties based on the exclusion list
|
||||
foreach (var remove in exclude)
|
||||
{
|
||||
if (contentType.PropertyTypeExists(remove))
|
||||
{
|
||||
contentType.RemovePropertyType(remove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override ContentItemDto<IMember> MapFromPersisted(MemberSave model)
|
||||
{
|
||||
return Mapper.Map<IMember, ContentItemDto<IMember>>(model.PersistedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs
Normal file
27
src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom validation helper so that we can exclude the Member.StandardPropertyTypeStubs from being validating for existence
|
||||
/// </summary>
|
||||
internal class MemberValidationHelper : ContentValidationHelper<IMember, MemberSave>
|
||||
{
|
||||
protected override bool ValidateProperties(ContentItemBasic<ContentPropertyBasic, IMember> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </remarks>
|
||||
[PluginController("UmbracoApi")]
|
||||
[UmbracoApplicationAuthorizeAttribute(Constants.Applications.Media)]
|
||||
[ContentModelFormatterConfiguration(typeof(MediaItemFormatter))]
|
||||
public class MediaController : ContentControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -214,9 +215,8 @@ namespace Umbraco.Web.Editors
|
||||
/// <returns></returns>
|
||||
[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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Web.Http;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
@@ -231,9 +232,8 @@ namespace Umbraco.Web.Editors
|
||||
/// <returns></returns>
|
||||
[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:
|
||||
|
||||
@@ -268,6 +268,7 @@
|
||||
</Compile>
|
||||
<Compile Include="ApplicationContextExtensions.cs" />
|
||||
<Compile Include="AreaRegistrationContextExtensions.cs" />
|
||||
<Compile Include="Editors\ContentModelValidationFilter.cs" />
|
||||
<Compile Include="Models\LegacyConvertedNode.cs" />
|
||||
<Compile Include="Models\LegacyConvertedNodeProperty.cs" />
|
||||
<Compile Include="Scheduling\BackgroundTaskRunner.cs" />
|
||||
@@ -547,12 +548,16 @@
|
||||
</Compile>
|
||||
<Compile Include="WebApi\AngularJsonMediaTypeFormatter.cs" />
|
||||
<Compile Include="WebApi\AngularJsonOnlyConfigurationAttribute.cs" />
|
||||
<Compile Include="WebApi\Binders\MemberBinder.cs" />
|
||||
<Compile Include="Editors\Filters\ContentItemBaseFormatter.cs" />
|
||||
<Compile Include="Editors\Filters\ContentItemFormatter.cs" />
|
||||
<Compile Include="Editors\Filters\MediaItemFormatter.cs" />
|
||||
<Compile Include="Editors\Filters\MemberFormatter.cs" />
|
||||
<Compile Include="WebApi\Filters\AngularAntiForgeryHelper.cs" />
|
||||
<Compile Include="WebApi\Filters\ClearAngularAntiForgeryTokenAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\DisableBrowserCacheAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\FilterGrouping.cs" />
|
||||
<Compile Include="WebApi\Filters\LegacyTreeAuthorizeAttribute.cs" />
|
||||
<Compile Include="Editors\Filters\MemberValidationHelper.cs" />
|
||||
<Compile Include="WebApi\Filters\OutgoingNoHyphenGuidFormatAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\SetAngularAntiForgeryTokensAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\UmbracoBackOfficeLogoutAttribute.cs" />
|
||||
@@ -791,16 +796,14 @@
|
||||
<Compile Include="UrlHelperExtensions.cs" />
|
||||
<Compile Include="Editors\MediaController.cs" />
|
||||
<Compile Include="UrlHelperRenderExtensions.cs" />
|
||||
<Compile Include="WebApi\Binders\ContentItemBaseBinder.cs" />
|
||||
<Compile Include="Editors\Filters\ContentItemBaseBinder.cs" />
|
||||
<Compile Include="WebApi\IsBackOfficeAttribute.cs" />
|
||||
<Compile Include="WebApi\Binders\ContentItemBinder.cs" />
|
||||
<Compile Include="WebApi\Binders\MediaItemBinder.cs" />
|
||||
<Compile Include="WebApi\Filters\EnsureUserPermissionForContentAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\ContentItemValidationHelper.cs" />
|
||||
<Compile Include="Editors\Filters\ContentValidationHelper.cs" />
|
||||
<Compile Include="WebApi\Filters\EnsureUserPermissionForMediaAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\FileUploadCleanupFilterAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\FilterAllowedOutgoingContentAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\FilterAllowedOutgoingMediaAttribute.cs" />
|
||||
<Compile Include="Editors\Filters\FilterAllowedOutgoingContentAttribute.cs" />
|
||||
<Compile Include="Editors\Filters\FilterAllowedOutgoingMediaAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\HttpQueryStringFilterAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\OutgoingDateTimeFormatAttribute.cs" />
|
||||
<Compile Include="WebApi\Filters\UmbracoApplicationAuthorizeAttribute.cs" />
|
||||
@@ -808,6 +811,7 @@
|
||||
<Compile Include="WebApi\HttpRequestMessageExtensions.cs" />
|
||||
<Compile Include="WebApi\JsonCamelCaseFormatter.cs" />
|
||||
<Compile Include="WebApi\MemberAuthorizeAttribute.cs" />
|
||||
<Compile Include="Editors\Filters\ContentModelFormatterConfigurationAttribute.cs" />
|
||||
<Compile Include="WebApi\NamespaceHttpControllerSelector.cs" />
|
||||
<Compile Include="WebApi\UmbracoApiController.cs" />
|
||||
<Compile Include="WebApi\UmbracoApiControllerResolver.cs" />
|
||||
|
||||
Reference in New Issue
Block a user