Merge branch '7.2.0-U4-4049' into dev-v7

Conflicts:
	src/Umbraco.Web/Umbraco.Web.csproj
This commit is contained in:
Shannon
2015-01-05 10:26:27 +11:00
18 changed files with 1037 additions and 784 deletions

View File

@@ -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;

View File

@@ -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

View 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; }
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View 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);
}
}
}

View File

@@ -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

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

View File

@@ -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:

View File

@@ -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" />