diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs
index 53c2adf7c2..f28f4766cc 100644
--- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs
+++ b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs
@@ -5,6 +5,8 @@
///
public class ContentPropertyFile
{
+ //TODO: This needs to be overhauled to support variants and things like NC
+
///
/// Gets or sets the property alias.
///
diff --git a/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs b/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs
index 7075e33fdd..6e9c731f20 100644
--- a/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs
+++ b/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs
@@ -123,7 +123,7 @@ namespace Umbraco.Tests.Models.Mapping
AssertBasics(result, content);
- var invariantContent = result.ContentVariants.First();
+ var invariantContent = result.Variants.First();
foreach (var p in content.Properties)
{
AssertBasicProperty(invariantContent, p);
@@ -146,7 +146,7 @@ namespace Umbraco.Tests.Models.Mapping
AssertBasics(result, content);
- var invariantContent = result.ContentVariants.First();
+ var invariantContent = result.Variants.First();
foreach (var p in content.Properties)
{
AssertBasicProperty(invariantContent, p);
@@ -195,7 +195,7 @@ namespace Umbraco.Tests.Models.Mapping
AssertBasics(result, content);
- var invariantContent = result.ContentVariants.First();
+ var invariantContent = result.Variants.First();
foreach (var p in content.Properties)
{
AssertBasicProperty(invariantContent, p);
@@ -238,7 +238,7 @@ namespace Umbraco.Tests.Models.Mapping
Assert.IsNull(result.Owner); // because, 0 is no user
}
- var invariantContent = result.ContentVariants.First();
+ var invariantContent = result.Variants.First();
Assert.AreEqual(content.ParentId, result.ParentId);
Assert.AreEqual(content.UpdateDate, invariantContent.UpdateDate);
diff --git a/src/Umbraco.Tests/Web/AngularIntegration/ContentModelSerializationTests.cs b/src/Umbraco.Tests/Web/AngularIntegration/ContentModelSerializationTests.cs
index 7d94943a6d..0c7908de9e 100644
--- a/src/Umbraco.Tests/Web/AngularIntegration/ContentModelSerializationTests.cs
+++ b/src/Umbraco.Tests/Web/AngularIntegration/ContentModelSerializationTests.cs
@@ -44,7 +44,7 @@ namespace Umbraco.Tests.Web.AngularIntegration
var displayModel = new ContentItemDisplay
{
Id = 1234,
- ContentVariants = new List
+ Variants = new List
{
new ContentVariantDisplay
{
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js
index 18d37ea5b2..67e7e0d2da 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js
@@ -343,6 +343,7 @@
/** formats the display model used to display the content to the model used to save the content */
formatContentPostData: function (displayModel, action) {
+ //TODO: We need to change this since it's no longer relevant with variants
//this is basically the same as for media but we need to explicitly add some extra properties
var saveModel = this.formatMediaPostData(displayModel, action);
diff --git a/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs b/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs
index e865019790..d7b8054532 100644
--- a/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs
+++ b/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs
@@ -33,12 +33,14 @@ namespace Umbraco.Web.Composing.CompositionRoots
//register any resolvers, etc.. that the profiles use
container.Register();
container.Register>();
- container.Register>();
+ container.Register>();
container.Register>();
container.Register>();
container.Register();
container.Register();
container.Register();
+ container.Register();
+ container.Register();
}
}
}
diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs b/src/Umbraco.Web/Editors/Binders/ContentItemBaseBinder.cs
similarity index 83%
rename from src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs
rename to src/Umbraco.Web/Editors/Binders/ContentItemBaseBinder.cs
index 34f8d90155..ec7158c10b 100644
--- a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs
+++ b/src/Umbraco.Web/Editors/Binders/ContentItemBaseBinder.cs
@@ -13,6 +13,7 @@ using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding.Binders;
using System.Web.Http.Validation;
using System.Web.ModelBinding;
+using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Umbraco.Core;
@@ -34,33 +35,33 @@ 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
+namespace Umbraco.Web.Editors.Binders
{
+ ///
///
/// Binds the content model to the controller action for the posted multi-part Post
///
internal abstract class ContentItemBaseBinder : IModelBinder
- where TPersisted : class, IContentBase
- where TModelSave : ContentBaseItemSave
+ where TPersisted : class, IContentBase
+ //where TModelSave : ContentBaseItemSave
{
protected Core.Logging.ILogger Logger { get; }
protected ServiceContext Services { get; }
protected IUmbracoContextAccessor UmbracoContextAccessor { get; }
- public ContentItemBaseBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
+ protected ContentItemBaseBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
{
}
- public ContentItemBaseBinder(Core.Logging.ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
+ protected ContentItemBaseBinder(Core.Logging.ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
{
Logger = logger;
Services = services;
UmbracoContextAccessor = umbracoContextAccessor;
}
- public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ public virtual bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
- //NOTE: Validation is done in the filter
if (actionContext.Request.Content.IsMimeMultipartContent() == false)
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
@@ -69,19 +70,11 @@ namespace Umbraco.Web.WebApi.Binders
var result = GetMultiPartResult(actionContext);
var model = GetModel(actionContext, bindingContext, result);
- //now that everything is binded, validate the properties
- var contentItemValidator = GetValidationHelper();
- contentItemValidator.ValidateItem(actionContext, model);
bindingContext.Model = model;
return bindingContext.Model != null;
}
- protected virtual ContentItemValidationHelper GetValidationHelper()
- {
- return new ContentItemValidationHelper(Logger, UmbracoContextAccessor);
- }
-
private MultipartFormDataStreamProvider GetMultiPartResult(HttpActionContext actionContext)
{
var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads");
@@ -129,14 +122,10 @@ namespace Umbraco.Web.WebApi.Binders
///
///
///
- ///
+ ///
///
private TModelSave GetModel(HttpActionContext actionContext, ModelBindingContext bindingContext, MultipartFormDataStreamProvider result)
{
- //var request = actionContext.Request;
- //var content = request.Content;
- //var result = content.ReadAsMultipartAsync(provider).ConfigureAwait(false).GetAwaiter().GetResult();
-
if (result.FormData["contentItem"] == null)
{
var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest);
@@ -170,14 +159,14 @@ namespace Umbraco.Web.WebApi.Binders
}
var propAlias = parts[1];
- var fileName = file.Headers.ContentDisposition.FileName.Trim(new char[] {'\"'});
+ var fileName = file.Headers.ContentDisposition.FileName.Trim(new char[] { '\"' });
model.UploadedFiles.Add(new ContentPropertyFile
- {
- TempFilePath = file.LocalFileName,
- PropertyAlias = propAlias,
- FileName = fileName
- });
+ {
+ TempFilePath = file.LocalFileName,
+ PropertyAlias = propAlias,
+ FileName = fileName
+ });
}
if (ContentControllerBase.IsCreatingAction(model.Action))
@@ -202,8 +191,6 @@ namespace Umbraco.Web.WebApi.Binders
MapPropertyValuesFromSaved(model, model.ContentDto);
}
- model.Name = model.Name.Trim();
-
return model;
}
@@ -212,7 +199,7 @@ namespace Umbraco.Web.WebApi.Binders
///
///
///
- private static void MapPropertyValuesFromSaved(TModelSave saveModel, ContentItemDto dto)
+ private static void MapPropertyValuesFromSaved(IContentProperties saveModel, ContentItemDto dto)
{
//NOTE: Don't convert this to linq, this is much quicker
foreach (var p in saveModel.Properties)
diff --git a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs
new file mode 100644
index 0000000000..df5569fb34
--- /dev/null
+++ b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using AutoMapper;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models;
+using Umbraco.Core.Services;
+using Umbraco.Web.Composing;
+using Umbraco.Web.Editors.Filters;
+using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Web.Models.Mapping;
+using Umbraco.Web.WebApi.Filters;
+
+namespace Umbraco.Web.Editors.Binders
+{
+ internal class ContentItemBinder : ContentItemBaseBinder
+ {
+ public ContentItemBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
+ {
+ }
+
+ public ContentItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
+ : base(logger, services, umbracoContextAccessor)
+ {
+ }
+
+ protected override IContent GetExisting(ContentItemSave model)
+ {
+ return Services.ContentService.GetById(model.Id);
+ }
+
+ protected override IContent CreateNew(ContentItemSave model)
+ {
+ var contentType = Services.ContentTypeService.Get(model.ContentTypeAlias);
+ if (contentType == null)
+ {
+ throw new InvalidOperationException("No content type found with alias " + model.ContentTypeAlias);
+ }
+ return new Content(
+ model.PersistedContent.ContentType.VariesByCulture() ? null : model.Variants.First().Name,
+ model.ParentId,
+ contentType);
+ }
+
+ protected override ContentItemDto MapFromPersisted(ContentItemSave model)
+ {
+ return MapFromPersisted(model.PersistedContent);
+ }
+
+ internal static ContentItemDto MapFromPersisted(IContent content)
+ {
+ return Mapper.Map>(content);
+ }
+
+
+ }
+}
diff --git a/src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs b/src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs
similarity index 62%
rename from src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs
rename to src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs
index 61dd0897db..c97c78dc71 100644
--- a/src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs
+++ b/src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs
@@ -1,4 +1,6 @@
using System;
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Models;
@@ -7,8 +9,12 @@ using Umbraco.Web.Composing;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Models.Mapping;
-namespace Umbraco.Web.WebApi.Binders
+namespace Umbraco.Web.Editors.Binders
{
+ ///
+ ///
+ /// The model binder for
+ ///
internal class MediaItemBinder : ContentItemBaseBinder
{
public MediaItemBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
@@ -20,6 +26,23 @@ namespace Umbraco.Web.WebApi.Binders
{
}
+ ///
+ /// Overridden to trim the name
+ ///
+ ///
+ ///
+ ///
+ public override bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ var result = base.BindModel(actionContext, bindingContext);
+ if (result)
+ {
+ var model = (MediaItemSave) bindingContext.Model;
+ model.Name = model.Name.Trim();
+ }
+ return result;
+ }
+
protected override IMedia GetExisting(MediaItemSave model)
{
return Services.MediaService.GetById(Convert.ToInt32(model.Id));
diff --git a/src/Umbraco.Web/Editors/Binders/MemberBinder.cs b/src/Umbraco.Web/Editors/Binders/MemberBinder.cs
new file mode 100644
index 0000000000..82647bb2a3
--- /dev/null
+++ b/src/Umbraco.Web/Editors/Binders/MemberBinder.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Net;
+using System.Web.Http;
+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 System.Net.Http;
+using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Services.Implement;
+using Umbraco.Web;
+using Umbraco.Web.Composing;
+using Umbraco.Core.Logging;
+using Umbraco.Web.Editors.Filters;
+
+namespace Umbraco.Web.Editors.Binders
+{
+ ///
+ ///
+ /// The model binder for
+ ///
+ internal class MemberBinder : ContentItemBaseBinder
+ {
+
+ public MemberBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
+ {
+ }
+
+ public MemberBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
+ : base(logger, services, umbracoContextAccessor)
+ {
+ }
+
+ ///
+ /// Overridden to trim the name
+ ///
+ ///
+ ///
+ ///
+ public override bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ var result = base.BindModel(actionContext, bindingContext);
+ if (result)
+ {
+ var model = (MemberSave)bindingContext.Model;
+ model.Name = model.Name.Trim();
+ }
+ return result;
+ }
+
+ ///
+ /// Returns an IMember instance used to bind values to and save (depending on the membership scenario)
+ ///
+ ///
+ ///
+ protected override IMember GetExisting(MemberSave model)
+ {
+ var scenario = Services.MemberService.GetMembershipScenario();
+ var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
+ switch (scenario)
+ {
+ case MembershipScenario.NativeUmbraco:
+ return GetExisting(model.Key);
+ case MembershipScenario.CustomProviderWithUmbracoLink:
+ case MembershipScenario.StandaloneCustomProvider:
+ default:
+ var membershipUser = provider.GetUser(model.Key, false);
+ if (membershipUser == null)
+ {
+ throw new InvalidOperationException("Could not find member with key " + model.Key);
+ }
+
+ //TODO: Support this scenario!
+ //if (scenario == MembershipScenario.CustomProviderWithUmbracoLink)
+ //{
+ // //if there's a 'Member' type then we should be able to just go get it from the db since it was created with a link
+ // // to our data.
+ // var memberType = ApplicationContext.Services.MemberTypeService.GetMemberType(Constants.Conventions.MemberTypes.Member);
+ // if (memberType != null)
+ // {
+ // var existing = GetExisting(model.Key);
+ // FilterContentTypeProperties(existing.ContentType, existing.ContentType.PropertyTypes.Select(x => x.Alias).ToArray());
+ // }
+ //}
+
+ //generate a member for a generic membership provider
+ //NOTE: We don't care about the password here, so just generate something
+ //var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"));
+
+ //var convertResult = membershipUser.ProviderUserKey.TryConvertTo();
+ //if (convertResult.Success == false)
+ //{
+ // throw new InvalidOperationException("Only membership providers that store a GUID as their ProviderUserKey are supported" + model.Key);
+ //}
+ //member.Key = convertResult.Result;
+
+ var member = Mapper.Map(membershipUser);
+
+ return member;
+ }
+ }
+
+ private IMember GetExisting(Guid key)
+ {
+ var member = Services.MemberService.GetByKey(key);
+ if (member == null)
+ {
+ throw new InvalidOperationException("Could not find member with key " + key);
+ }
+
+ return member;
+ }
+
+ ///
+ /// Gets an instance of IMember used when creating a member
+ ///
+ ///
+ ///
+ ///
+ /// Depending on whether a custom membership provider is configured this will return different results.
+ ///
+ protected override IMember CreateNew(MemberSave model)
+ {
+ var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
+
+ if (provider.IsUmbracoMembershipProvider())
+ {
+ var contentType = 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 = Services.MemberTypeService.Get(Constants.Conventions.MemberTypes.DefaultAlias);
+ if (memberType != null)
+ {
+ FilterContentTypeProperties(memberType, memberType.PropertyTypes.Select(x => x.Alias).ToArray());
+ return new Member(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"), memberType);
+ }
+
+ //generate a member for a generic membership provider
+ var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"));
+ //we'll just remove all properties here otherwise we'll end up with validation errors, we don't want to persist any property data anyways
+ // in this case.
+ FilterContentTypeProperties(member.ContentType, member.ContentType.PropertyTypes.Select(x => x.Alias).ToArray());
+ return member;
+ }
+ }
+
+ ///
+ /// This will remove all of the special membership provider properties which were required to display the property editors
+ /// for editing - but the values have been mapped back ot the MemberSave object directly - we don't want to keep these properties
+ /// on the IMember because they will attempt to be persisted which we don't want since they might not even exist.
+ ///
+ ///
+ private void FilterMembershipProviderProperties(IContentTypeBase contentType)
+ {
+ var defaultProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs();
+ //remove all membership properties, these values are set with the membership provider.
+ var exclude = defaultProps.Select(x => x.Value.Alias).ToArray();
+ FilterContentTypeProperties(contentType, exclude);
+ }
+
+ private void FilterContentTypeProperties(IContentTypeBase contentType, IEnumerable exclude)
+ {
+ //remove all properties based on the exclusion list
+ foreach (var remove in exclude)
+ {
+ if (contentType.PropertyTypeExists(remove))
+ {
+ contentType.RemovePropertyType(remove);
+ }
+ }
+ }
+
+ protected override ContentItemDto MapFromPersisted(MemberSave model)
+ {
+ //need to explicitly cast since it's an explicit implementation
+ var saveModel = (IContentSave)model;
+
+ return Mapper.Map>(saveModel.PersistedContent);
+ }
+
+
+ }
+}
diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs
index 92d392e890..423027d6a0 100644
--- a/src/Umbraco.Web/Editors/ContentController.cs
+++ b/src/Umbraco.Web/Editors/ContentController.cs
@@ -31,6 +31,7 @@ using Constants = Umbraco.Core.Constants;
using ContentVariation = Umbraco.Core.Models.ContentVariation;
using Language = Umbraco.Web.Models.ContentEditing.Language;
using Umbraco.Core.PropertyEditors;
+using Umbraco.Web.Editors.Filters;
namespace Umbraco.Web.Editors
{
@@ -230,7 +231,7 @@ namespace Umbraco.Web.Editors
ContentTypeAlias = "recycleBin",
IsContainer = true,
Path = "-1," + Constants.System.RecycleBinContent,
- ContentVariants = new List
+ Variants = new List
{
new ContentVariantDisplay
{
@@ -293,7 +294,7 @@ namespace Umbraco.Web.Editors
HandleContentNotFound(id);
return null;//irrelevant since the above throws
}
- var content = MapToDisplay(foundContent, culture);
+ var content = MapToDisplay(foundContent);
return content;
}
@@ -304,7 +305,7 @@ namespace Umbraco.Web.Editors
///
[OutgoingEditorModelEvent]
[EnsureUserPermissionForContent("id")]
- public ContentItemDisplay GetById(Guid id, string culture = null)
+ public ContentItemDisplay GetById(Guid id)
{
var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id));
if (foundContent == null)
@@ -324,12 +325,12 @@ namespace Umbraco.Web.Editors
///
[OutgoingEditorModelEvent]
[EnsureUserPermissionForContent("id")]
- public ContentItemDisplay GetById(Udi id, string culture = null)
+ public ContentItemDisplay GetById(Udi id)
{
var guidUdi = id as GuidUdi;
if (guidUdi != null)
{
- return GetById(guidUdi.Guid, culture);
+ return GetById(guidUdi.Guid);
}
throw new HttpResponseException(HttpStatusCode.NotFound);
@@ -631,119 +632,117 @@ namespace Umbraco.Web.Editors
private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod)
{
- throw new NotImplementedException("Implement this!");
+ //If we've reached here it means:
+ // * Our model has been bound
+ // * and validated
+ // * any file attachments have been saved to their temporary location for us to use
+ // * we have a reference to the DTO object and the persisted object
+ // * Permissions are valid
+ MapPropertyValues(contentItem);
- ////If we've reached here it means:
- //// * Our model has been bound
- //// * and validated
- //// * any file attachments have been saved to their temporary location for us to use
- //// * we have a reference to the DTO object and the persisted object
- //// * Permissions are valid
- //MapPropertyValues(contentItem);
+ //We need to manually check the validation results here because:
+ // * We still need to save the entity even if there are validation value errors
+ // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null)
+ // then we cannot continue saving, we can only display errors
+ // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display
+ // a message indicating this
+ if (ModelState.IsValid == false)
+ {
+ if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(contentItem) && IsCreatingAction(contentItem.Action))
+ {
+ //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
+ // add the modelstate to the outgoing object and throw a validation message
+ var forDisplay = MapToDisplay(contentItem.PersistedContent, contentItem.Culture);
+ forDisplay.Errors = ModelState.ToErrorDictionary();
+ throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay));
- ////We need to manually check the validation results here because:
- //// * We still need to save the entity even if there are validation value errors
- //// * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null)
- //// then we cannot continue saving, we can only display errors
- //// * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display
- //// a message indicating this
- //if (ModelState.IsValid == false)
- //{
- // if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(contentItem) && IsCreatingAction(contentItem.Action))
- // {
- // //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
- // // add the modelstate to the outgoing object and throw a validation message
- // var forDisplay = MapToDisplay(contentItem.PersistedContent, contentItem.Culture);
- // forDisplay.Errors = ModelState.ToErrorDictionary();
- // throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay));
+ }
- // }
+ //if the model state is not valid we cannot publish so change it to save
+ switch (contentItem.Action)
+ {
+ case ContentSaveAction.Publish:
+ contentItem.Action = ContentSaveAction.Save;
+ break;
+ case ContentSaveAction.PublishNew:
+ contentItem.Action = ContentSaveAction.SaveNew;
+ break;
+ }
+ }
- // //if the model state is not valid we cannot publish so change it to save
- // switch (contentItem.Action)
- // {
- // case ContentSaveAction.Publish:
- // contentItem.Action = ContentSaveAction.Save;
- // break;
- // case ContentSaveAction.PublishNew:
- // contentItem.Action = ContentSaveAction.SaveNew;
- // break;
- // }
- //}
+ //initialize this to successful
+ var publishStatus = new PublishResult(null, contentItem.PersistedContent);
+ var wasCancelled = false;
- ////initialize this to successful
- //var publishStatus = new PublishResult(null, contentItem.PersistedContent);
- //var wasCancelled = false;
+ if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew)
+ {
+ //save the item
+ var saveResult = saveMethod(contentItem.PersistedContent);
- //if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew)
- //{
- // //save the item
- // var saveResult = saveMethod(contentItem.PersistedContent);
+ wasCancelled = saveResult.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent;
+ }
+ else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew)
+ {
+ var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id);
+ wasCancelled = sendResult == false;
+ }
+ else
+ {
+ PublishInternal(contentItem, ref publishStatus, ref wasCancelled);
+ }
- // wasCancelled = saveResult.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent;
- //}
- //else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew)
- //{
- // var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id);
- // wasCancelled = sendResult == false;
- //}
- //else
- //{
- // PublishInternal(contentItem, ref publishStatus, ref wasCancelled);
- //}
+ //get the updated model
+ var display = MapToDisplay(contentItem.PersistedContent, contentItem.Culture);
- ////get the updated model
- //var display = MapToDisplay(contentItem.PersistedContent, contentItem.Culture);
+ //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
+ HandleInvalidModelState(display);
- ////lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
- //HandleInvalidModelState(display);
+ //put the correct msgs in
+ switch (contentItem.Action)
+ {
+ case ContentSaveAction.Save:
+ case ContentSaveAction.SaveNew:
+ if (wasCancelled == false)
+ {
+ display.AddSuccessNotification(
+ Services.TextService.Localize("speechBubbles/editContentSavedHeader"),
+ Services.TextService.Localize("speechBubbles/editContentSavedText"));
+ }
+ else
+ {
+ AddCancelMessage(display);
+ }
+ break;
+ case ContentSaveAction.SendPublish:
+ case ContentSaveAction.SendPublishNew:
+ if (wasCancelled == false)
+ {
+ display.AddSuccessNotification(
+ Services.TextService.Localize("speechBubbles/editContentSendToPublish"),
+ Services.TextService.Localize("speechBubbles/editContentSendToPublishText"));
+ }
+ else
+ {
+ AddCancelMessage(display);
+ }
+ break;
+ case ContentSaveAction.Publish:
+ case ContentSaveAction.PublishNew:
+ ShowMessageForPublishStatus(publishStatus, display);
+ break;
+ }
- ////put the correct msgs in
- //switch (contentItem.Action)
- //{
- // case ContentSaveAction.Save:
- // case ContentSaveAction.SaveNew:
- // if (wasCancelled == false)
- // {
- // display.AddSuccessNotification(
- // Services.TextService.Localize("speechBubbles/editContentSavedHeader"),
- // Services.TextService.Localize("speechBubbles/editContentSavedText"));
- // }
- // else
- // {
- // AddCancelMessage(display);
- // }
- // break;
- // case ContentSaveAction.SendPublish:
- // case ContentSaveAction.SendPublishNew:
- // if (wasCancelled == false)
- // {
- // display.AddSuccessNotification(
- // Services.TextService.Localize("speechBubbles/editContentSendToPublish"),
- // Services.TextService.Localize("speechBubbles/editContentSendToPublishText"));
- // }
- // else
- // {
- // AddCancelMessage(display);
- // }
- // break;
- // case ContentSaveAction.Publish:
- // case ContentSaveAction.PublishNew:
- // ShowMessageForPublishStatus(publishStatus, display);
- // break;
- //}
+ //If the item is new and the operation was cancelled, we need to return a different
+ // status code so the UI can handle it since it won't be able to redirect since there
+ // is no Id to redirect to!
+ if (wasCancelled && IsCreatingAction(contentItem.Action))
+ {
+ throw new HttpResponseException(Request.CreateValidationErrorResponse(display));
+ }
- ////If the item is new and the operation was cancelled, we need to return a different
- //// status code so the UI can handle it since it won't be able to redirect since there
- //// is no Id to redirect to!
- //if (wasCancelled && IsCreatingAction(contentItem.Action))
- //{
- // throw new HttpResponseException(Request.CreateValidationErrorResponse(display));
- //}
+ display.PersistedContent = contentItem.PersistedContent;
- //display.PersistedContent = contentItem.PersistedContent;
-
- //return display;
+ return display;
}
///
@@ -1078,51 +1077,56 @@ namespace Umbraco.Web.Editors
///
/// Maps the dto property values to the persisted model
///
- ///
- private void MapPropertyValues(ContentItemSave contentItem)
+ ///
+ private void MapPropertyValues(ContentItemSave contentSave)
{
- //Don't update the name if it is empty
- if (!contentItem.Name.IsNullOrWhiteSpace())
+ //set the names for the variants
+ foreach(var variant in contentSave.Variants)
{
- if (contentItem.PersistedContent.ContentType.VariesByCulture())
+ //Don't update the name if it is empty
+ if (!variant.Name.IsNullOrWhiteSpace())
{
- if (contentItem.Culture.IsNullOrWhiteSpace())
- throw new InvalidOperationException($"Cannot set culture name without a culture.");
- contentItem.PersistedContent.SetCultureName(contentItem.Name, contentItem.Culture);
- }
- else
- {
- contentItem.PersistedContent.Name = contentItem.Name;
+ if (contentSave.PersistedContent.ContentType.VariesByCulture())
+ {
+ if (variant.Culture.IsNullOrWhiteSpace())
+ throw new InvalidOperationException($"Cannot set culture name without a culture.");
+ contentSave.PersistedContent.SetCultureName(variant.Name, variant.Culture);
+ }
+ else
+ {
+ contentSave.PersistedContent.Name = variant.Name;
+ }
}
}
//TODO: We need to support 'send to publish'
- contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate;
- contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate;
+ contentSave.PersistedContent.ExpireDate = contentSave.ExpireDate;
+ contentSave.PersistedContent.ReleaseDate = contentSave.ReleaseDate;
+
//only set the template if it didn't change
- var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false)
- || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias)
- || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace());
+ var templateChanged = (contentSave.PersistedContent.Template == null && contentSave.TemplateAlias.IsNullOrWhiteSpace() == false)
+ || (contentSave.PersistedContent.Template != null && contentSave.PersistedContent.Template.Alias != contentSave.TemplateAlias)
+ || (contentSave.PersistedContent.Template != null && contentSave.TemplateAlias.IsNullOrWhiteSpace());
if (templateChanged)
{
- var template = Services.FileService.GetTemplate(contentItem.TemplateAlias);
- if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false)
+ var template = Services.FileService.GetTemplate(contentSave.TemplateAlias);
+ if (template == null && contentSave.TemplateAlias.IsNullOrWhiteSpace() == false)
{
//ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias);
- Logger.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias);
+ Logger.Warn("No template exists with the specified alias: " + contentSave.TemplateAlias);
}
else
{
//NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template
- contentItem.PersistedContent.Template = template;
+ contentSave.PersistedContent.Template = template;
}
}
bool Varies(Property property) => property.PropertyType.VariesByCulture();
MapPropertyValues(
- contentItem,
+ contentSave,
(save, property) => Varies(property) ? property.GetValue(save.Culture) : property.GetValue(), //get prop val
(save, property, v) => { if (Varies(property)) property.SetValue(v, save.Culture); else property.SetValue(v); }); //set prop val
}
@@ -1323,19 +1327,7 @@ namespace Umbraco.Web.Editors
///
private ContentItemDisplay MapToDisplay(IContent content)
{
- ////A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited,
- //// the Cuture property of ContentItemDisplay must exist (at least currently).
- //if (culture == null && content.ContentType.VariesByCulture())
- //{
- // //If a culture is not explicitly sent up, then it means that the user is editing the default variant language.
- // culture = Services.LocalizationService.GetDefaultLanguageIsoCode();
- //}
-
- //var display = ContextMapper.Map(content, UmbracoContext,
- // new Dictionary { { ContextMapper.CultureKey, culture } });
-
- var display = ContextMapper.Map(content, UmbracoContext);
-
+ var display = Mapper.Map(content);
return display;
}
}
diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs
index 6171ccaefc..afed9cb776 100644
--- a/src/Umbraco.Web/Editors/ContentControllerBase.cs
+++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs
@@ -49,7 +49,7 @@ namespace Umbraco.Web.Editors
Func getPropertyValue,
Action savePropertyValue)
where TPersisted : IContentBase
- where TSaved : ContentBaseItemSave
+ where TSaved : ContentBaseSave
{
// map the property values
foreach (var propertyDto in contentItem.ContentDto.Properties)
diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs
index 2bd52da5d2..313693c08c 100644
--- a/src/Umbraco.Web/Editors/EntityController.cs
+++ b/src/Umbraco.Web/Editors/EntityController.cs
@@ -18,6 +18,7 @@ using Umbraco.Core.Models.Entities;
using Umbraco.Core.Xml;
using Umbraco.Web.Search;
using Umbraco.Web.Trees;
+using Umbraco.Web.WebApi;
namespace Umbraco.Web.Editors
{
diff --git a/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs b/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs
deleted file mode 100644
index cae0759141..0000000000
--- a/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System;
-using System.Web.Http.Controllers;
-using Umbraco.Web.WebApi;
-
-namespace Umbraco.Web.Editors
-{
-
-}
diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs b/src/Umbraco.Web/Editors/Filters/ContentItemValidationHelper.cs
similarity index 76%
rename from src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs
rename to src/Umbraco.Web/Editors/Filters/ContentItemValidationHelper.cs
index 3eaf28a77b..d5c3384b87 100644
--- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs
+++ b/src/Umbraco.Web/Editors/Filters/ContentItemValidationHelper.cs
@@ -5,26 +5,16 @@ 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.Composing;
using Umbraco.Web.Models.ContentEditing;
-namespace Umbraco.Web.WebApi.Filters
+namespace Umbraco.Web.Editors.Filters
{
///
- /// A validation helper class used with ContentItemValidationFilterAttribute to be shared between content, media, etc...
+ /// A base class purely used for logging without generics
///
- ///
- ///
- ///
- /// If any severe errors occur then the response gets set to an error and execution will not continue. Property validation
- /// errors will just be added to the ModelState.
- ///
- internal class ContentItemValidationHelper
- where TPersisted : class, IContentBase
- where TModelSave : ContentBaseItemSave
+ internal class ContentItemValidationHelper
{
protected IUmbracoContextAccessor UmbracoContextAccessor { get; }
protected ILogger Logger { get; }
@@ -34,6 +24,24 @@ namespace Umbraco.Web.WebApi.Filters
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
UmbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
}
+ }
+
+ ///
+ /// A validation helper class used with ContentItemValidationFilterAttribute to be shared between content, media, etc...
+ ///
+ ///
+ ///
+ ///
+ /// If any severe errors occur then the response gets set to an error and execution will not continue. Property validation
+ /// errors will just be added to the ModelState.
+ ///
+ internal class ContentItemValidationHelper: ContentItemValidationHelper
+ where TPersisted : class, IContentBase
+ where TModelSave: IContentSave, IContentProperties
+ {
+ public ContentItemValidationHelper(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) : base(logger, umbracoContextAccessor)
+ {
+ }
///
/// Validates the content item and updates the Response and ModelState accordingly
@@ -42,28 +50,22 @@ namespace Umbraco.Web.WebApi.Filters
///
public void ValidateItem(HttpActionContext actionContext, string argumentName)
{
- if (!(actionContext.ActionArguments[argumentName] is TModelSave contentItem))
+ if (!(actionContext.ActionArguments[argumentName] is TModelSave model))
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "No " + typeof(TModelSave) + " found in request");
return;
}
- ValidateItem(actionContext, contentItem);
+ ValidateItem(actionContext, model);
}
- public void ValidateItem(HttpActionContext actionContext, TModelSave contentItem)
+ public void ValidateItem(HttpActionContext actionContext, TModelSave model)
{
//now do each validation step
- if (ValidateExistingContent(contentItem, actionContext) == false) return;
- if (ValidateCultureVariant(contentItem, actionContext) == false) return;
- if (ValidateProperties(contentItem, actionContext) == false) return;
- if (ValidatePropertyData(contentItem, contentItem.ContentDto, actionContext.ModelState) == false) return;
- }
-
- protected virtual bool ValidateCultureVariant(TModelSave postedItem, HttpActionContext actionContext)
- {
- return true;
+ if (ValidateExistingContent(model, actionContext) == false) return;
+ if (ValidateProperties(model, actionContext) == false) return;
+ if (ValidatePropertyData(model, actionContext.ModelState) == false) return;
}
///
@@ -74,10 +76,10 @@ namespace Umbraco.Web.WebApi.Filters
///
protected virtual bool ValidateExistingContent(TModelSave postedItem, HttpActionContext actionContext)
{
- if (postedItem.PersistedContent == null)
+ var persistedContent = postedItem.PersistedContent;
+ if (persistedContent == null)
{
- var message = $"content with id: {postedItem.Id} was not found";
- actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
+ actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "content was not found");
return false;
}
@@ -87,12 +89,13 @@ namespace Umbraco.Web.WebApi.Filters
///
/// Ensure all of the ids in the post are valid
///
- ///
+ ///
///
///
- protected virtual bool ValidateProperties(TModelSave postedItem, HttpActionContext actionContext)
+ protected virtual bool ValidateProperties(TModelSave model, HttpActionContext actionContext)
{
- return ValidateProperties(postedItem.Properties.ToList(), postedItem.PersistedContent.Properties.ToList(), actionContext);
+ var persistedContent = model.PersistedContent;
+ return ValidateProperties(model.Properties.ToList(), persistedContent.Properties.ToList(), actionContext);
}
///
@@ -123,15 +126,17 @@ namespace Umbraco.Web.WebApi.Filters
///
/// Validates the data for each property
///
- ///
- ///
+ ///
///
///
///
/// All property data validation goes into the modelstate with a prefix of "Properties"
///
- public virtual bool ValidatePropertyData(TModelSave postedItem, ContentItemDto dto, ModelStateDictionary modelState)
+ public virtual bool ValidatePropertyData(TModelSave model, ModelStateDictionary modelState)
{
+ var properties = model.Properties.ToList();
+ var dto = model.ContentDto;
+
foreach (var p in dto.Properties)
{
var editor = p.PropertyEditor;
@@ -140,13 +145,13 @@ namespace Umbraco.Web.WebApi.Filters
{
var message = $"Could not find property editor \"{p.DataType.EditorAlias}\" for property with id {p.Id}.";
- Logger.Warn>(message);
+ Logger.Warn(message);
continue;
}
//get the posted value for this property, this may be null in cases where the property was marked as readonly which means
//the angular app will not post that value.
- var postedProp = postedItem.Properties.FirstOrDefault(x => x.Alias == p.Alias);
+ var postedProp = properties.FirstOrDefault(x => x.Alias == p.Alias);
if (postedProp == null) continue;
var postedValue = postedProp.Value;
diff --git a/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs b/src/Umbraco.Web/Editors/Filters/ContentPostValidateAttribute.cs
similarity index 98%
rename from src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs
rename to src/Umbraco.Web/Editors/Filters/ContentPostValidateAttribute.cs
index 17d78f2422..c044ad0b5c 100644
--- a/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs
+++ b/src/Umbraco.Web/Editors/Filters/ContentPostValidateAttribute.cs
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Umbraco.Core;
@@ -13,7 +11,7 @@ using Umbraco.Web.Security;
using Umbraco.Web.WebApi;
using Umbraco.Web._Legacy.Actions;
-namespace Umbraco.Web.Editors
+namespace Umbraco.Web.Editors.Filters
{
///
/// Checks if the user has access to post a content item based on whether it's being created or saved.
diff --git a/src/Umbraco.Web/Editors/IsCurrentUserModelFilterAttribute.cs b/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs
similarity index 98%
rename from src/Umbraco.Web/Editors/IsCurrentUserModelFilterAttribute.cs
rename to src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs
index a791be7aed..59a383dca6 100644
--- a/src/Umbraco.Web/Editors/IsCurrentUserModelFilterAttribute.cs
+++ b/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs
@@ -3,7 +3,7 @@ using System.Net.Http;
using System.Web.Http.Filters;
using Umbraco.Web.Models.ContentEditing;
-namespace Umbraco.Web.Editors
+namespace Umbraco.Web.Editors.Filters
{
///
/// This sets the IsCurrentUser property on any outgoing model or any collection of models
diff --git a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs
new file mode 100644
index 0000000000..c63bdd5dfe
--- /dev/null
+++ b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs
@@ -0,0 +1,98 @@
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models;
+using Umbraco.Core.Services;
+using Umbraco.Web.Composing;
+using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Web.WebApi;
+
+namespace Umbraco.Web.Editors.Filters
+{
+ ///
+ /// Validates the incoming model
+ ///
+ internal class MediaItemSaveValidationAttribute : ActionFilterAttribute
+ {
+ private readonly ILogger _logger;
+ private readonly IUmbracoContextAccessor _umbracoContextAccessor;
+ private readonly IMediaService _mediaService;
+ private readonly IEntityService _entityService;
+
+ public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.MediaService, Current.Services.EntityService)
+ {
+ }
+
+ public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, IMediaService mediaService, IEntityService entityService)
+ {
+ _logger = logger;
+ _umbracoContextAccessor = umbracoContextAccessor;
+ _mediaService = mediaService;
+ _entityService = entityService;
+ }
+
+ public override void OnActionExecuting(HttpActionContext actionContext)
+ {
+ var model = (MediaItemSave)actionContext.ActionArguments["contentItem"];
+ var contentItemValidator = new ContentItemValidationHelper(_logger, _umbracoContextAccessor);
+
+ if (ValidateUserAccess(model, actionContext))
+ contentItemValidator.ValidateItem(actionContext, model);
+ }
+
+ ///
+ /// Checks if the user has access to post a content item based on whether it's being created or saved.
+ ///
+ ///
+ ///
+ private bool ValidateUserAccess(MediaItemSave mediaItem, HttpActionContext actionContext)
+ {
+ //We now need to validate that the user is allowed to be doing what they are doing.
+ //Then if it is new, we need to lookup those permissions on the parent.
+ IMedia contentToCheck;
+ int contentIdToCheck;
+ switch (mediaItem.Action)
+ {
+ case ContentSaveAction.Save:
+ contentToCheck = mediaItem.PersistedContent;
+ contentIdToCheck = contentToCheck.Id;
+ break;
+ case ContentSaveAction.SaveNew:
+ contentToCheck = _mediaService.GetById(mediaItem.ParentId);
+
+ if (mediaItem.ParentId != Constants.System.Root)
+ {
+ contentToCheck = _mediaService.GetById(mediaItem.ParentId);
+ contentIdToCheck = contentToCheck.Id;
+ }
+ else
+ {
+ contentIdToCheck = mediaItem.ParentId;
+ }
+
+ break;
+ default:
+ //we don't support this for media
+ actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.NotFound);
+ return false;
+ }
+
+ if (MediaController.CheckPermissions(
+ actionContext.Request.Properties,
+ _umbracoContextAccessor.UmbracoContext.Security.CurrentUser,
+ _mediaService, _entityService,
+ contentIdToCheck, contentToCheck) == false)
+ {
+ actionContext.Response = actionContext.Request.CreateUserNoAccessResponse();
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs
new file mode 100644
index 0000000000..187bf7e1c8
--- /dev/null
+++ b/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs
@@ -0,0 +1,38 @@
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models;
+using Umbraco.Web.Composing;
+using Umbraco.Web.Models.ContentEditing;
+
+namespace Umbraco.Web.Editors.Filters
+{
+ ///
+ /// Validates the incoming model
+ ///
+ internal class MemberSaveValidationAttribute : ActionFilterAttribute
+ {
+ private readonly ILogger _logger;
+ private readonly IUmbracoContextAccessor _umbracoContextAccessor;
+
+ public MemberSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor)
+ {
+ }
+
+ public MemberSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor)
+ {
+ _logger = logger;
+ _umbracoContextAccessor = umbracoContextAccessor;
+ }
+
+ public override void OnActionExecuting(HttpActionContext actionContext)
+ {
+ var model = (MemberSave)actionContext.ActionArguments["contentItem"];
+ var contentItemValidator = new MemberValidationHelper(_logger, _umbracoContextAccessor);
+ contentItemValidator.ValidateItem(actionContext, model);
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs b/src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs
new file mode 100644
index 0000000000..264574453a
--- /dev/null
+++ b/src/Umbraco.Web/Editors/Filters/MemberValidationHelper.cs
@@ -0,0 +1,224 @@
+using System;
+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 System.Web.Security;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models;
+using Umbraco.Web.Models.ContentEditing;
+
+namespace Umbraco.Web.Editors.Filters
+{
+
+ //internal class ContentValidationHelper : ContentItemValidationHelper
+ //{
+ // public ContentValidationHelper(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) : base(logger, umbracoContextAccessor)
+ // {
+ // }
+
+ // ///
+ // /// Validates that the correct information is in the request for saving a culture variant
+ // ///
+ // ///
+ // ///
+ // ///
+ // protected override bool ValidateCultureVariant(ContentItemSave postedItem, HttpActionContext actionContext)
+ // {
+ // var contentType = postedItem.PersistedContent.GetContentType();
+ // if (contentType.VariesByCulture() && postedItem.Culture.IsNullOrWhiteSpace())
+ // {
+ // //we cannot save a content item that is culture variant if no culture was specified in the request!
+ // actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No culture found in request. Cannot save a content item that varies by culture, without a specified culture.");
+ // return false;
+ // }
+ // return true;
+ // }
+ //}
+
+ ///
+ /// Custom validation helper so that we can exclude the Member.StandardPropertyTypeStubs from being validating for existence
+ ///
+ internal class MemberValidationHelper : ContentItemValidationHelper
+ {
+ public MemberValidationHelper(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) : base(logger, umbracoContextAccessor)
+ {
+ }
+
+ ///
+ /// We need to manually validate a few things here like email and login to make sure they are valid and aren't duplicates
+ ///
+ ///
+ ///
+ ///
+ public override bool ValidatePropertyData(MemberSave model, ModelStateDictionary modelState)
+ {
+ if (model.Username.IsNullOrWhiteSpace())
+ {
+ modelState.AddPropertyError(
+ new ValidationResult("Invalid user name", new[] { "value" }),
+ $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
+ }
+
+ if (model.Email.IsNullOrWhiteSpace() || new EmailAddressAttribute().IsValid(model.Email) == false)
+ {
+ modelState.AddPropertyError(
+ new ValidationResult("Invalid email", new[] { "value" }),
+ $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
+ }
+
+ //default provider!
+ var membershipProvider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
+
+ var validEmail = ValidateUniqueEmail(model, membershipProvider);
+ if (validEmail == false)
+ {
+ modelState.AddPropertyError(
+ new ValidationResult("Email address is already in use", new[] { "value" }),
+ $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
+ }
+
+ var validLogin = ValidateUniqueLogin(model, membershipProvider);
+ if (validLogin == false)
+ {
+ modelState.AddPropertyError(
+ new ValidationResult("Username is already in use", new[] { "value" }),
+ $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
+ }
+
+ return base.ValidatePropertyData(model, modelState);
+ }
+
+ ///
+ /// This ensures that the internal membership property types are removed from validation before processing the validation
+ /// since those properties are actually mapped to real properties of the IMember.
+ /// This also validates any posted data for fields that are sensitive.
+ ///
+ ///
+ ///
+ ///
+ protected override bool ValidateProperties(MemberSave model, HttpActionContext actionContext)
+ {
+ var propertiesToValidate = model.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);
+ }
+
+ //if the user doesn't have access to sensitive values, then we need to validate the incoming properties to check
+ //if a sensitive value is being submitted.
+ if (UmbracoContextAccessor.UmbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false)
+ {
+ var sensitiveProperties = model.PersistedContent.ContentType
+ .PropertyTypes.Where(x => model.PersistedContent.ContentType.IsSensitiveProperty(x.Alias))
+ .ToList();
+
+ foreach (var sensitiveProperty in sensitiveProperties)
+ {
+ var prop = propertiesToValidate.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
+
+ if (prop != null)
+ {
+ //this should not happen, this means that there was data posted for a sensitive property that
+ //the user doesn't have access to, which means that someone is trying to hack the values.
+
+ var message = $"property with alias: {prop.Alias} cannot be posted";
+ actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new InvalidOperationException(message));
+ return false;
+ }
+ }
+ }
+
+ return ValidateProperties(propertiesToValidate, model.PersistedContent.Properties.ToList(), actionContext);
+ }
+
+ internal bool ValidateUniqueLogin(MemberSave model, MembershipProvider membershipProvider)
+ {
+ if (model == null) throw new ArgumentNullException(nameof(model));
+ if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider));
+
+ int totalRecs;
+ var existingByName = membershipProvider.FindUsersByName(model.Username.Trim(), 0, int.MaxValue, out totalRecs);
+ switch (model.Action)
+ {
+ case ContentSaveAction.Save:
+
+ //ok, we're updating the member, we need to check if they are changing their login and if so, does it exist already ?
+ if (model.PersistedContent.Username.InvariantEquals(model.Username.Trim()) == false)
+ {
+ //they are changing their login name
+ if (existingByName.Cast().Select(x => x.UserName)
+ .Any(x => x == model.Username.Trim()))
+ {
+ //the user cannot use this login
+ return false;
+ }
+ }
+ break;
+ case ContentSaveAction.SaveNew:
+ //check if the user's login already exists
+ if (existingByName.Cast().Select(x => x.UserName)
+ .Any(x => x == model.Username.Trim()))
+ {
+ //the user cannot use this login
+ return false;
+ }
+ break;
+ default:
+ //we don't support this for members
+ throw new ArgumentOutOfRangeException();
+ }
+
+ return true;
+ }
+
+ internal bool ValidateUniqueEmail(MemberSave model, MembershipProvider membershipProvider)
+ {
+ if (model == null) throw new ArgumentNullException(nameof(model));
+ if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider));
+
+ if (membershipProvider.RequiresUniqueEmail == false)
+ {
+ return true;
+ }
+
+ int totalRecs;
+ var existingByEmail = membershipProvider.FindUsersByEmail(model.Email.Trim(), 0, int.MaxValue, out totalRecs);
+ switch (model.Action)
+ {
+ case ContentSaveAction.Save:
+ //ok, we're updating the member, we need to check if they are changing their email and if so, does it exist already ?
+ if (model.PersistedContent.Email.InvariantEquals(model.Email.Trim()) == false)
+ {
+ //they are changing their email
+ if (existingByEmail.Cast().Select(x => x.Email)
+ .Any(x => x.InvariantEquals(model.Email.Trim())))
+ {
+ //the user cannot use this email
+ return false;
+ }
+ }
+ break;
+ case ContentSaveAction.SaveNew:
+ //check if the user's email already exists
+ if (existingByEmail.Cast().Select(x => x.Email)
+ .Any(x => x.InvariantEquals(model.Email.Trim())))
+ {
+ //the user cannot use this email
+ return false;
+ }
+ break;
+ default:
+ //we don't support this for members
+ throw new ArgumentOutOfRangeException();
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Editors/UserGroupAuthorizationAttribute.cs b/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs
similarity index 97%
rename from src/Umbraco.Web/Editors/UserGroupAuthorizationAttribute.cs
rename to src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs
index 6b0bac0d69..4293c31660 100644
--- a/src/Umbraco.Web/Editors/UserGroupAuthorizationAttribute.cs
+++ b/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs
@@ -1,13 +1,12 @@
using System;
using System.Linq;
-using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using Umbraco.Core;
using Umbraco.Core.Composing;
-namespace Umbraco.Web.Editors
+namespace Umbraco.Web.Editors.Filters
{
///
/// Authorizes that the current user has access to the user group Id in the request
diff --git a/src/Umbraco.Web/Editors/UserGroupEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs
similarity index 99%
rename from src/Umbraco.Web/Editors/UserGroupEditorAuthorizationHelper.cs
rename to src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs
index 26dfb9fb1f..2b2bf337de 100644
--- a/src/Umbraco.Web/Editors/UserGroupEditorAuthorizationHelper.cs
+++ b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs
@@ -5,7 +5,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
-namespace Umbraco.Web.Editors
+namespace Umbraco.Web.Editors.Filters
{
internal class UserGroupEditorAuthorizationHelper
{
diff --git a/src/Umbraco.Web/Editors/UserGroupValidateAttribute.cs b/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs
similarity index 98%
rename from src/Umbraco.Web/Editors/UserGroupValidateAttribute.cs
rename to src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs
index b3bc59554d..f062be1aff 100644
--- a/src/Umbraco.Web/Editors/UserGroupValidateAttribute.cs
+++ b/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs
@@ -11,7 +11,7 @@ using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.WebApi;
-namespace Umbraco.Web.Editors
+namespace Umbraco.Web.Editors.Filters
{
internal sealed class UserGroupValidateAttribute : ActionFilterAttribute
{
diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs
index 2d5ac19ba8..1259ce9639 100644
--- a/src/Umbraco.Web/Editors/MediaController.cs
+++ b/src/Umbraco.Web/Editors/MediaController.cs
@@ -23,7 +23,6 @@ using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using System.Linq;
using System.Web.Http.Controllers;
-using Umbraco.Web.WebApi.Binders;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
using Umbraco.Core.Configuration;
@@ -35,6 +34,8 @@ using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.Models.Validation;
using Umbraco.Core.PropertyEditors;
+using Umbraco.Web.Editors.Binders;
+using Umbraco.Web.Editors.Filters;
namespace Umbraco.Web.Editors
{
@@ -81,7 +82,7 @@ namespace Umbraco.Web.Editors
}
var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0));
- var mapped = ContextMapper.Map(emptyContent, UmbracoContext);
+ var mapped = Mapper.Map(emptyContent);
//remove the listview app if it exists
mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "childItems").ToList();
@@ -131,7 +132,7 @@ namespace Umbraco.Web.Editors
//HandleContentNotFound will throw an exception
return null;
}
- return ContextMapper.Map(foundContent, UmbracoContext);
+ return Mapper.Map(foundContent);
}
///
@@ -151,7 +152,7 @@ namespace Umbraco.Web.Editors
//HandleContentNotFound will throw an exception
return null;
}
- return ContextMapper.Map(foundContent, UmbracoContext);
+ return Mapper.Map(foundContent);
}
///
@@ -180,7 +181,7 @@ namespace Umbraco.Web.Editors
public IEnumerable GetByIds([FromUri]int[] ids)
{
var foundMedia = Services.MediaService.GetByIds(ids);
- return foundMedia.Select(media => ContextMapper.Map(media, UmbracoContext));
+ return foundMedia.Select(media => Mapper.Map(media));
}
///
@@ -430,7 +431,7 @@ namespace Umbraco.Web.Editors
///
///
[FileUploadCleanupFilter]
- [MediaPostValidate]
+ [MediaItemSaveValidation]
[OutgoingEditorModelEvent]
public MediaItemDisplay PostSave(
[ModelBinder(typeof(MediaItemBinder))]
@@ -467,7 +468,7 @@ namespace Umbraco.Web.Editors
{
//ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
// add the modelstate to the outgoing object and throw validation response
- var forDisplay = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext);
+ var forDisplay = Mapper.Map(contentItem.PersistedContent);
forDisplay.Errors = ModelState.ToErrorDictionary();
throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay));
}
@@ -477,7 +478,7 @@ namespace Umbraco.Web.Editors
var saveStatus = Services.MediaService.Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id);
//return the updated model
- var display = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext);
+ var display = Mapper.Map(contentItem.PersistedContent);
//lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
HandleInvalidModelState(display);
@@ -574,7 +575,7 @@ namespace Umbraco.Web.Editors
var f = mediaService.CreateMedia(folder.Name, intParentId, Constants.Conventions.MediaTypes.Folder);
mediaService.Save(f, Security.CurrentUser.Id);
- return ContextMapper.Map(f, UmbracoContext);
+ return Mapper.Map(f);
}
///
diff --git a/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs b/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs
deleted file mode 100644
index d153283025..0000000000
--- a/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using System;
-using System.Net;
-using System.Net.Http;
-using System.Web.Http;
-using System.Web.Http.Controllers;
-using System.Web.Http.Filters;
-using Umbraco.Core;
-using Umbraco.Core.Models;
-using Umbraco.Core.Services;
-using Umbraco.Web.Composing;
-using Umbraco.Web.Models.ContentEditing;
-using Umbraco.Web.Security;
-using Umbraco.Web.WebApi;
-
-namespace Umbraco.Web.Editors
-{
- ///
- /// Checks if the user has access to post a content item based on whether it's being created or saved.
- ///
- internal sealed class MediaPostValidateAttribute : ActionFilterAttribute
- {
- private readonly IMediaService _mediaService;
- private readonly IEntityService _entityService;
- private readonly WebSecurity _security;
-
- public MediaPostValidateAttribute()
- {
- }
-
- // fixme wtf is this?
- public MediaPostValidateAttribute(IMediaService mediaService, IEntityService entityService, WebSecurity security)
- {
- _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
- _entityService = entityService;
- _security = security ?? throw new ArgumentNullException(nameof(security));
- }
-
- // fixme all these should be injected properties
-
- private IMediaService MediaService
- => _mediaService ?? Current.Services.MediaService;
-
- private IEntityService EntityService
- => _entityService ?? Current.Services.EntityService;
-
- private WebSecurity Security
- => _security ?? UmbracoContext.Current.Security;
-
- public override void OnActionExecuting(HttpActionContext actionContext)
- {
- var mediaItem = (MediaItemSave)actionContext.ActionArguments["contentItem"];
-
- //We now need to validate that the user is allowed to be doing what they are doing.
- //Then if it is new, we need to lookup those permissions on the parent.
- IMedia contentToCheck;
- int contentIdToCheck;
- switch (mediaItem.Action)
- {
- case ContentSaveAction.Save:
- contentToCheck = mediaItem.PersistedContent;
- contentIdToCheck = contentToCheck.Id;
- break;
- case ContentSaveAction.SaveNew:
- contentToCheck = MediaService.GetById(mediaItem.ParentId);
-
- if (mediaItem.ParentId != Constants.System.Root)
- {
- contentToCheck = MediaService.GetById(mediaItem.ParentId);
- contentIdToCheck = contentToCheck.Id;
- }
- else
- {
- contentIdToCheck = mediaItem.ParentId;
- }
-
- break;
- default:
- //we don't support this for media
- actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.NotFound);
- return;
- }
-
- if (MediaController.CheckPermissions(
- actionContext.Request.Properties,
- Security.CurrentUser,
- MediaService, EntityService,
- contentIdToCheck, contentToCheck) == false)
- {
- throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse());
- }
- }
- }
-}
diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs
index 7cf0e73e3c..61a7f3baa9 100644
--- a/src/Umbraco.Web/Editors/MemberController.cs
+++ b/src/Umbraco.Web/Editors/MemberController.cs
@@ -23,11 +23,12 @@ 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 Constants = Umbraco.Core.Constants;
using System.Collections.Generic;
using Umbraco.Core.PropertyEditors;
+using Umbraco.Web.Editors.Binders;
+using Umbraco.Web.Editors.Filters;
namespace Umbraco.Web.Editors
{
@@ -84,7 +85,7 @@ namespace Umbraco.Web.Editors
var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize)
{
Items = members
- .Select(x => ContextMapper.Map(x, UmbracoContext))
+ .Select(x => Mapper.Map(x))
};
return pagedResult;
}
@@ -173,7 +174,7 @@ namespace Umbraco.Web.Editors
{
HandleContentNotFound(key);
}
- return ContextMapper.Map(foundMember, UmbracoContext);
+ return Mapper.Map(foundMember);
case MembershipScenario.CustomProviderWithUmbracoLink:
//TODO: Support editing custom properties for members with a custom membership provider here.
@@ -233,7 +234,7 @@ namespace Umbraco.Web.Editors
emptyContent = new Member(contentType);
emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(provider.MinRequiredPasswordLength, provider.MinRequiredNonAlphanumericCharacters);
- return ContextMapper.Map(emptyContent, UmbracoContext);
+ return Mapper.Map(emptyContent);
case MembershipScenario.CustomProviderWithUmbracoLink:
//TODO: Support editing custom properties for members with a custom membership provider here.
@@ -242,7 +243,7 @@ namespace Umbraco.Web.Editors
//we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit
emptyContent = MemberService.CreateGenericMembershipProviderMember("", "", "", "");
emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters);
- return ContextMapper.Map(emptyContent, UmbracoContext);
+ return Mapper.Map(emptyContent);
}
}
@@ -252,6 +253,7 @@ namespace Umbraco.Web.Editors
///
[FileUploadCleanupFilter]
[OutgoingEditorModelEvent]
+ [MemberSaveValidation]
public MemberDisplay PostSave(
[ModelBinder(typeof(MemberBinder))]
MemberSave contentItem)
@@ -282,7 +284,7 @@ namespace Umbraco.Web.Editors
//Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
if (ModelState.IsValid == false)
{
- var forDisplay = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext);
+ var forDisplay = Mapper.Map(contentItem.PersistedContent);
forDisplay.Errors = ModelState.ToErrorDictionary();
throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay));
}
@@ -324,7 +326,7 @@ namespace Umbraco.Web.Editors
//If we've had problems creating/updating the user with the provider then return the error
if (ModelState.IsValid == false)
{
- var forDisplay = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext);
+ var forDisplay = Mapper.Map(contentItem.PersistedContent);
forDisplay.Errors = ModelState.ToErrorDictionary();
throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay));
}
@@ -362,7 +364,7 @@ namespace Umbraco.Web.Editors
contentItem.PersistedContent.AdditionalData["GeneratedPassword"] = generatedPassword;
//return the updated model
- var display = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext);
+ var display = Mapper.Map(contentItem.PersistedContent);
//lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
HandleInvalidModelState(display);
@@ -545,6 +547,7 @@ namespace Umbraco.Web.Editors
/// Re-fetches the database data to map to the PersistedContent object and re-assigns the already mapped the posted properties so that the display object is up-to-date
///
///
+ ///
///
/// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data
///
diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs
index 83f5db08ad..677890bcc4 100644
--- a/src/Umbraco.Web/Editors/UserGroupsController.cs
+++ b/src/Umbraco.Web/Editors/UserGroupsController.cs
@@ -9,6 +9,7 @@ using AutoMapper;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
+using Umbraco.Web.Editors.Filters;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs
index 73a4d9c910..28972ba7f6 100644
--- a/src/Umbraco.Web/Editors/UsersController.cs
+++ b/src/Umbraco.Web/Editors/UsersController.cs
@@ -25,6 +25,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
+using Umbraco.Web.Editors.Filters;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentBaseItemSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentBaseItemSave.cs
deleted file mode 100644
index 3a28793ac7..0000000000
--- a/src/Umbraco.Web/Models/ContentEditing/ContentBaseItemSave.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Runtime.Serialization;
-using Umbraco.Core.Models;
-using Umbraco.Core.Models.Editors;
-
-namespace Umbraco.Web.Models.ContentEditing
-{
- ///
- /// A model representing a content base item to be saved
- ///
- [DataContract(Name = "content", Namespace = "")]
- public abstract class ContentBaseItemSave : ContentItemBasic, IHaveUploadedFiles
- where TPersisted : IContentBase
- {
- protected ContentBaseItemSave()
- {
- UploadedFiles = new List();
- }
-
- ///
- /// The action to perform when saving this content item
- ///
- [DataMember(Name = "action", IsRequired = true)]
- [Required]
- public ContentSaveAction Action { get; set; }
-
- [IgnoreDataMember]
- public List UploadedFiles { get; private set; }
- }
-}
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentBaseSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentBaseSave.cs
new file mode 100644
index 0000000000..8748a3f4e0
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentBaseSave.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Editors;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ ///
+ /// A model representing a content item to be saved
+ ///
+ [DataContract(Name = "content", Namespace = "")]
+ public abstract class ContentBaseSave : ContentItemBasic, IContentSave
+ where TPersisted : IContentBase
+ {
+ protected ContentBaseSave()
+ {
+ UploadedFiles = new List();
+ }
+
+ #region IContentSave
+ ///
+ [DataMember(Name = "action", IsRequired = true)]
+ [Required]
+ public ContentSaveAction Action { get; set; }
+
+ [IgnoreDataMember]
+ public List UploadedFiles { get; }
+
+ //These need explicit implementation because we are using internal models
+ ///
+ [IgnoreDataMember]
+ TPersisted IContentSave.PersistedContent { get; set; }
+
+ //These need explicit implementation because we are using internal models
+ ///
+ [IgnoreDataMember]
+ ContentItemDto IContentSave.ContentDto { get; set; }
+
+ //Non explicit internal getter so we don't need to explicitly cast in our own code
+ [IgnoreDataMember]
+ internal TPersisted PersistedContent
+ {
+ get => ((IContentSave)this).PersistedContent;
+ set => ((IContentSave) this).PersistedContent = value;
+ }
+
+ //Non explicit internal getter so we don't need to explicitly cast in our own code
+ [IgnoreDataMember]
+ internal ContentItemDto ContentDto
+ {
+ get => ((IContentSave)this).ContentDto;
+ set => ((IContentSave) this).ContentDto = value;
+ }
+
+ #endregion
+
+ }
+}
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
index 5b4f710a28..12764de918 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
@@ -85,26 +85,5 @@ namespace Umbraco.Web.Models.ContentEditing
set => _properties = value;
}
- ///
- /// The real persisted content object - used during inbound model binding
- ///
- ///
- /// This is not used for outgoing model information.
- ///
- [IgnoreDataMember]
- internal TPersisted PersistedContent { get; set; }
-
- ///
- /// The DTO object used to gather all required content data including data type information etc... for use with validation - used during inbound model binding
- ///
- ///
- /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need
- /// instead of having to look up all the data individually.
- /// This is not used for outgoing model information.
- ///
- [IgnoreDataMember]
- internal ContentItemDto ContentDto { get; set; }
-
-
}
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
index 8f14114c68..1b1c5a3f8d 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
@@ -22,7 +22,7 @@ namespace Umbraco.Web.Models.ContentEditing
AllowPreview = true;
Notifications = new List();
Errors = new Dictionary();
- ContentVariants = new List();
+ Variants = new List();
ContentApps = new List();
}
@@ -65,7 +65,7 @@ namespace Umbraco.Web.Models.ContentEditing
/// If a content item is invariant, this collection will only contain one item, else it will contain all culture variants
///
[DataMember(Name = "variants")]
- public IEnumerable ContentVariants { get; set; }
+ public IEnumerable Variants { get; set; }
[DataMember(Name = "owner")]
public UserProfile Owner { get; set; }
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs
index fa0bbaba3a..ba1560978c 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Umbraco.Core.Models;
+using Umbraco.Core.Models.Editors;
namespace Umbraco.Web.Models.ContentEditing
{
@@ -10,13 +12,28 @@ namespace Umbraco.Web.Models.ContentEditing
/// A model representing a content item to be saved
///
[DataContract(Name = "content", Namespace = "")]
- public class ContentItemSave : ContentBaseItemSave
+ public class ContentItemSave : IContentSave
{
- ///
- /// The language Id for the content variation being saved
- ///
- [DataMember(Name = "culture")]
- public string Culture { get; set; } //TODO: Change this to ContentVariationPublish, but this will all change anyways when we can edit all variants at once
+ protected ContentItemSave()
+ {
+ UploadedFiles = new List();
+ Variants = new List();
+ }
+
+ [DataMember(Name = "id", IsRequired = true)]
+ [Required]
+ public int Id { get; set; }
+
+ [DataMember(Name = "parentId", IsRequired = true)]
+ [Required]
+ public int ParentId { get; set; }
+
+ [DataMember(Name = "variants", IsRequired = true)]
+ public IEnumerable Variants { get; set; }
+
+ [DataMember(Name = "contentTypeAlias", IsRequired = true)]
+ [Required(AllowEmptyStrings = false)]
+ public string ContentTypeAlias { get; set; }
///
/// The template alias to save
@@ -24,16 +41,48 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "templateAlias")]
public string TemplateAlias { get; set; }
+ //TODO: these will need to move to the variant
[DataMember(Name = "releaseDate")]
public DateTime? ReleaseDate { get; set; }
-
[DataMember(Name = "expireDate")]
public DateTime? ExpireDate { get; set; }
- ///
- /// Indicates that these variations should also be published
- ///
- [DataMember(Name = "publishVariations")]
- public IEnumerable PublishVariations { get; set; }
+ #region IContentSave
+
+ [DataMember(Name = "action", IsRequired = true)]
+ [Required]
+ public ContentSaveAction Action { get; set; }
+
+ [IgnoreDataMember]
+ public List UploadedFiles { get; }
+
+ //These need explicit implementation because we are using internal models
+ ///
+ [IgnoreDataMember]
+ IContent IContentSave.PersistedContent { get; set; }
+
+ //These need explicit implementation because we are using internal models
+ ///
+ [IgnoreDataMember]
+ ContentItemDto IContentSave.ContentDto { get; set; }
+
+ //Non explicit internal getter so we don't need to explicitly cast in our own code
+ [IgnoreDataMember]
+ internal IContent PersistedContent
+ {
+ get => ((IContentSave)this).PersistedContent;
+ set => ((IContentSave)this).PersistedContent = value;
+ }
+
+ //Non explicit internal getter so we don't need to explicitly cast in our own code
+ [IgnoreDataMember]
+ internal ContentItemDto ContentDto
+ {
+ get => ((IContentSave)this).ContentDto;
+ set => ((IContentSave)this).ContentDto = value;
+ }
+
+ #endregion
+
}
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs
new file mode 100644
index 0000000000..72a59f05cc
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Validation;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ [DataContract(Name = "contentVariant", Namespace = "")]
+ public class ContentVariantSave : IContentProperties
+ {
+ public ContentVariantSave()
+ {
+ Properties = new List();
+ }
+
+ [DataMember(Name = "name", IsRequired = true)]
+ [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")]
+ public string Name { get; set; }
+
+ [DataMember(Name = "properties")]
+ public IEnumerable Properties { get; set; }
+
+ ///
+ /// The culture of this variant, if this is invariant than this is null or empty
+ ///
+ [DataMember(Name = "culture")]
+ public string Culture { get; set; }
+
+ ///
+ /// Indicates if the variant should be published or unpublished
+ ///
+ [DataMember(Name = "publish")]
+ public bool Publish { get; set; }
+
+
+ }
+}
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs
index e6f32bae35..6e95991580 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs
@@ -4,10 +4,10 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;
-using Umbraco.Core.Models;
namespace Umbraco.Web.Models.ContentEditing
{
+
///
/// Represents the variant info for a content item
///
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs
deleted file mode 100644
index 71c4672ccb..0000000000
--- a/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using System.Runtime.Serialization;
-
-namespace Umbraco.Web.Models.ContentEditing
-{
- ///
- /// Used to indicate which additional variants to publish when a content item is published
- ///
- public class ContentVariationPublish
- {
- [DataMember(Name = "culture", IsRequired = true)]
- [Required]
- public string Culture { get; set; }
-
- [DataMember(Name = "segment")]
- public string Segment { get; set; }
- }
-}
diff --git a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs
index 6c574bc8bd..0c633d0319 100644
--- a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs
@@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
+using System.Web.Http.ModelBinding;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Models.Validation;
using Umbraco.Core.Serialization;
+using Umbraco.Web.WebApi;
namespace Umbraco.Web.Models.ContentEditing
{
diff --git a/src/Umbraco.Web/Models/ContentEditing/IContentProperties.cs b/src/Umbraco.Web/Models/ContentEditing/IContentProperties.cs
index 409c758536..dbb1c1da5e 100644
--- a/src/Umbraco.Web/Models/ContentEditing/IContentProperties.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/IContentProperties.cs
@@ -1,7 +1,24 @@
using System.Collections.Generic;
+using Umbraco.Core.Models;
namespace Umbraco.Web.Models.ContentEditing
{
+ /////
+ ///// An interface that needs to be implemented for the ContentItemValidationHelper to be used
+ /////
+ /////
+ /////
+ ///// We want to share the validation and model binding logic with content, media and members but because of variants content
+ ///// is now quite different than the others so this allows us to continue sharing the logic between these models.
+ /////
+ //internal interface IContentItemValidationHelperModel
+ // where TPersisted : IContentBase
+ //{
+ // TPersisted GetPersisted();
+ // ContentItemDto GetDto();
+ // IContentProperties GetProperties();
+ //}
+
public interface IContentProperties
where T : ContentPropertyBasic
{
diff --git a/src/Umbraco.Web/Models/ContentEditing/IContentSave.cs b/src/Umbraco.Web/Models/ContentEditing/IContentSave.cs
new file mode 100644
index 0000000000..cf4d448131
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/IContentSave.cs
@@ -0,0 +1,35 @@
+using Umbraco.Core.Models;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ ///
+ /// An interface exposes the shared parts of content, media, members that we use during model binding in order to share logic
+ ///
+ ///
+ internal interface IContentSave : IHaveUploadedFiles
+ where TPersisted : IContentBase
+ {
+ ///
+ /// The action to perform when saving this content item
+ ///
+ ContentSaveAction Action { get; set; }
+
+ ///
+ /// The real persisted content object - used during inbound model binding
+ ///
+ ///
+ /// This is not used for outgoing model information.
+ ///
+ TPersisted PersistedContent { get; set; }
+
+ ///
+ /// The DTO object used to gather all required content data including data type information etc... for use with validation - used during inbound model binding
+ ///
+ ///
+ /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need
+ /// instead of having to look up all the data individually.
+ /// This is not used for outgoing model information.
+ ///
+ ContentItemDto ContentDto { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaItemSave.cs b/src/Umbraco.Web/Models/ContentEditing/MediaItemSave.cs
index 334c0d1ddd..b31f1d1782 100644
--- a/src/Umbraco.Web/Models/ContentEditing/MediaItemSave.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/MediaItemSave.cs
@@ -1,5 +1,6 @@
using System.Runtime.Serialization;
using Umbraco.Core.Models;
+using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Models.ContentEditing
{
@@ -7,8 +8,7 @@ namespace Umbraco.Web.Models.ContentEditing
/// A model representing a media item to be saved
///
[DataContract(Name = "content", Namespace = "")]
- public class MediaItemSave : ContentBaseItemSave
+ public class MediaItemSave : ContentBaseSave
{
-
}
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs b/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs
index d66ba8600f..b25a210083 100644
--- a/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs
@@ -3,14 +3,14 @@ using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Models;
+using Umbraco.Core.Models.Editors;
using Umbraco.Core.Models.Validation;
+using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Models.ContentEditing
{
- ///
- /// A model representing a member to be saved
- ///
- public class MemberSave : ContentBaseItemSave
+ ///
+ public class MemberSave : ContentBaseSave
{
[DataMember(Name = "username", IsRequired = true)]
@@ -35,6 +35,7 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "isApproved")]
public bool IsApproved { get; set; }
+
//TODO: Need to add question / answer support
}
diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs
index 4b49a21606..0e2c3640ac 100644
--- a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs
+++ b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs
@@ -19,6 +19,7 @@ namespace Umbraco.Web.Models.Mapping
ContentUrlResolver contentUrlResolver,
ContentTreeNodeUrlResolver contentTreeNodeUrlResolver,
TabsAndPropertiesResolver tabsAndPropertiesResolver,
+ ContentAppResolver contentAppResolver,
IUserService userService,
ILocalizedTextService textService,
IContentService contentService,
@@ -35,14 +36,13 @@ namespace Umbraco.Web.Models.Mapping
var contentTypeBasicResolver = new ContentTypeBasicResolver();
var defaultTemplateResolver = new DefaultTemplateResolver();
var variantResolver = new ContentVariantResolver(localizationService, textService);
- var contentAppResolver = new ContentAppResolver(dataTypeService, propertyEditors);
-
+
//FROM IContent TO ContentItemDisplay
CreateMap()
.ForMember(dest => dest.Udi, opt => opt.MapFrom(src => Udi.Create(src.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document, src.Key)))
.ForMember(dest => dest.Owner, opt => opt.ResolveUsing(src => contentOwnerResolver.Resolve(src)))
.ForMember(dest => dest.Updater, opt => opt.ResolveUsing(src => creatorResolver.Resolve(src)))
- .ForMember(dest => dest.ContentVariants, opt => opt.ResolveUsing(variantResolver))
+ .ForMember(dest => dest.Variants, opt => opt.ResolveUsing(variantResolver))
.ForMember(dest => dest.ContentApps, opt => opt.ResolveUsing(contentAppResolver))
.ForMember(dest => dest.Icon, opt => opt.MapFrom(src => src.ContentType.Icon))
.ForMember(dest => dest.ContentTypeAlias, opt => opt.MapFrom(src => src.ContentType.Alias))
diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs
index ffcd39856e..b85ccd1523 100644
--- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs
+++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs
@@ -58,13 +58,14 @@ namespace Umbraco.Web.Models.Mapping
// if it isn't one of the ones specified in 'includeProperties', we will just return the result without mapping the Value.
if (context.Options.Items.ContainsKey("IncludeProperties"))
{
- var includeProperties = context.Options.Items["IncludeProperties"] as IEnumerable;
- if (includeProperties != null && includeProperties.Contains(property.Alias) == false)
+ if (context.Options.Items["IncludeProperties"] is IEnumerable includeProperties
+ && includeProperties.Contains(property.Alias) == false)
{
return result;
}
}
+ //Get the culture from the context which will be set during the mapping operation for each property
var culture = context.GetCulture();
//a culture needs to be in the context for a property type that can vary
diff --git a/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs
index fa0f576ee9..b4bae19e00 100644
--- a/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs
+++ b/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs
@@ -18,6 +18,7 @@ namespace Umbraco.Web.Models.Mapping
public MediaMapperProfile(
TabsAndPropertiesResolver tabsAndPropertiesResolver,
ContentTreeNodeUrlResolver contentTreeNodeUrlResolver,
+ MediaAppResolver mediaAppResolver,
IUserService userService,
ILocalizedTextService textService,
IDataTypeService dataTypeService,
@@ -29,7 +30,6 @@ namespace Umbraco.Web.Models.Mapping
var mediaOwnerResolver = new OwnerResolver(userService);
var childOfListViewResolver = new MediaChildOfListViewResolver(mediaService, mediaTypeService);
var mediaTypeBasicResolver = new ContentTypeBasicResolver();
- var mediaAppResolver = new MediaAppResolver(dataTypeService, propertyEditors);
//FROM IMedia TO MediaItemDisplay
CreateMap()
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index b754d7ed26..667e1a69af 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -115,6 +115,8 @@
+
+
@@ -122,11 +124,12 @@
+
-
+
-
+
@@ -135,10 +138,10 @@
-
-
+
+
-
+
@@ -207,8 +210,8 @@
+
-
@@ -455,6 +458,8 @@
+
+
@@ -608,13 +613,12 @@
-
+
-
@@ -634,7 +638,7 @@
-
+
@@ -733,7 +737,6 @@
-
@@ -815,7 +818,7 @@
-
+
@@ -997,12 +1000,12 @@
-
+
-
-
+
+
-
+
diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs
deleted file mode 100644
index 6e80ba52f7..0000000000
--- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
-using System.Web.Http.Controllers;
-using AutoMapper;
-using Umbraco.Core;
-using Umbraco.Core.Logging;
-using Umbraco.Core.Models;
-using Umbraco.Core.Services;
-using Umbraco.Web.Composing;
-using Umbraco.Web.Models.ContentEditing;
-using Umbraco.Web.Models.Mapping;
-using Umbraco.Web.WebApi.Filters;
-
-namespace Umbraco.Web.WebApi.Binders
-{
- internal class ContentItemBinder : ContentItemBaseBinder
- {
- public ContentItemBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
- {
- }
-
- public ContentItemBinder(Core.Logging.ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
- : base(logger, services, umbracoContextAccessor)
- {
- }
-
- protected override ContentItemValidationHelper GetValidationHelper()
- {
- return new ContentValidationHelper(Logger, UmbracoContextAccessor);
- }
-
- protected override IContent GetExisting(ContentItemSave model)
- {
- return Services.ContentService.GetById(Convert.ToInt32(model.Id));
- }
-
- protected override IContent CreateNew(ContentItemSave model)
- {
- var contentType = Services.ContentTypeService.Get(model.ContentTypeAlias);
- if (contentType == null)
- {
- throw new InvalidOperationException("No content type found with alias " + model.ContentTypeAlias);
- }
- return new Content(model.Name, model.ParentId, contentType);
- }
-
- protected override ContentItemDto MapFromPersisted(ContentItemSave model)
- {
- return MapFromPersisted(model.PersistedContent, model.Culture);
- }
-
- internal static ContentItemDto MapFromPersisted(IContent content, string culture)
- {
- return ContextMapper.Map>(content, new Dictionary
- {
- [ContextMapper.CultureKey] = culture
- });
- }
-
- internal class ContentValidationHelper : ContentItemValidationHelper
- {
- public ContentValidationHelper(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) : base(logger, umbracoContextAccessor)
- {
- }
-
- ///
- /// Validates that the correct information is in the request for saving a culture variant
- ///
- ///
- ///
- ///
- protected override bool ValidateCultureVariant(ContentItemSave postedItem, HttpActionContext actionContext)
- {
- var contentType = postedItem.PersistedContent.GetContentType();
- if (contentType.VariesByCulture() && postedItem.Culture.IsNullOrWhiteSpace())
- {
- //we cannot save a content item that is culture variant if no culture was specified in the request!
- actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No culture found in request. Cannot save a content item that varies by culture, without a specified culture.");
- return false;
- }
- return true;
- }
- }
- }
-}
diff --git a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs
deleted file mode 100644
index df97891e3b..0000000000
--- a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs
+++ /dev/null
@@ -1,374 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Net;
-using System.Web.Http;
-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 System.Net.Http;
-using Umbraco.Core.Models.Membership;
-using Umbraco.Core.Services.Implement;
-using Umbraco.Web;
-using Umbraco.Web.Composing;
-using Umbraco.Core.Logging;
-
-namespace Umbraco.Web.WebApi.Binders
-{
- internal class MemberBinder : ContentItemBaseBinder
- {
-
- public MemberBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
- {
- }
-
- public MemberBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
- : base(logger, services, umbracoContextAccessor)
- {
- }
-
- protected override ContentItemValidationHelper GetValidationHelper()
- {
- return new MemberValidationHelper(Logger, UmbracoContextAccessor);
- }
-
- ///
- /// Returns an IMember instance used to bind values to and save (depending on the membership scenario)
- ///
- ///
- ///
- protected override IMember GetExisting(MemberSave model)
- {
- var scenario = Services.MemberService.GetMembershipScenario();
- var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
- switch (scenario)
- {
- case MembershipScenario.NativeUmbraco:
- return GetExisting(model.Key);
- case MembershipScenario.CustomProviderWithUmbracoLink:
- case MembershipScenario.StandaloneCustomProvider:
- default:
- var membershipUser = provider.GetUser(model.Key, false);
- if (membershipUser == null)
- {
- throw new InvalidOperationException("Could not find member with key " + model.Key);
- }
-
- //TODO: Support this scenario!
- //if (scenario == MembershipScenario.CustomProviderWithUmbracoLink)
- //{
- // //if there's a 'Member' type then we should be able to just go get it from the db since it was created with a link
- // // to our data.
- // var memberType = ApplicationContext.Services.MemberTypeService.GetMemberType(Constants.Conventions.MemberTypes.Member);
- // if (memberType != null)
- // {
- // var existing = GetExisting(model.Key);
- // FilterContentTypeProperties(existing.ContentType, existing.ContentType.PropertyTypes.Select(x => x.Alias).ToArray());
- // }
- //}
-
- //generate a member for a generic membership provider
- //NOTE: We don't care about the password here, so just generate something
- //var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"));
-
- //var convertResult = membershipUser.ProviderUserKey.TryConvertTo();
- //if (convertResult.Success == false)
- //{
- // throw new InvalidOperationException("Only membership providers that store a GUID as their ProviderUserKey are supported" + model.Key);
- //}
- //member.Key = convertResult.Result;
-
- var member = Mapper.Map(membershipUser);
-
- return member;
- }
- }
-
- private IMember GetExisting(Guid key)
- {
- var member = Services.MemberService.GetByKey(key);
- if (member == null)
- {
- throw new InvalidOperationException("Could not find member with key " + key);
- }
-
- return member;
- }
-
- ///
- /// Gets an instance of IMember used when creating a member
- ///
- ///
- ///
- ///
- /// Depending on whether a custom membership provider is configured this will return different results.
- ///
- protected override IMember CreateNew(MemberSave model)
- {
- var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
-
- if (provider.IsUmbracoMembershipProvider())
- {
- var contentType = 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 = Services.MemberTypeService.Get(Constants.Conventions.MemberTypes.DefaultAlias);
- if (memberType != null)
- {
- FilterContentTypeProperties(memberType, memberType.PropertyTypes.Select(x => x.Alias).ToArray());
- return new Member(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"), memberType);
- }
-
- //generate a member for a generic membership provider
- var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"));
- //we'll just remove all properties here otherwise we'll end up with validation errors, we don't want to persist any property data anyways
- // in this case.
- FilterContentTypeProperties(member.ContentType, member.ContentType.PropertyTypes.Select(x => x.Alias).ToArray());
- return member;
- }
- }
-
- ///
- /// This will remove all of the special membership provider properties which were required to display the property editors
- /// for editing - but the values have been mapped back ot the MemberSave object directly - we don't want to keep these properties
- /// on the IMember because they will attempt to be persisted which we don't want since they might not even exist.
- ///
- ///
- private void FilterMembershipProviderProperties(IContentTypeBase contentType)
- {
- var defaultProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs();
- //remove all membership properties, these values are set with the membership provider.
- var exclude = defaultProps.Select(x => x.Value.Alias).ToArray();
- FilterContentTypeProperties(contentType, exclude);
- }
-
- private void FilterContentTypeProperties(IContentTypeBase contentType, IEnumerable exclude)
- {
- //remove all properties based on the exclusion list
- foreach (var remove in exclude)
- {
- if (contentType.PropertyTypeExists(remove))
- {
- contentType.RemovePropertyType(remove);
- }
- }
- }
-
- protected override ContentItemDto MapFromPersisted(MemberSave model)
- {
- return Mapper.Map>(model.PersistedContent);
- }
-
- ///
- /// Custom validation helper so that we can exclude the Member.StandardPropertyTypeStubs from being validating for existence
- ///
- internal class MemberValidationHelper : ContentItemValidationHelper
- {
- public MemberValidationHelper(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) : base(logger, umbracoContextAccessor)
- {
- }
-
- ///
- /// We need to manually validate a few things here like email and login to make sure they are valid and aren't duplicates
- ///
- ///
- ///
- ///
- ///
- public override bool ValidatePropertyData(MemberSave postedItem, ContentItemDto dto, ModelStateDictionary modelState)
- {
- var memberSave = postedItem;
-
- if (memberSave.Username.IsNullOrWhiteSpace())
- {
- modelState.AddPropertyError(
- new ValidationResult("Invalid user name", new[] { "value" }),
- $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
- }
-
- if (memberSave.Email.IsNullOrWhiteSpace() || new EmailAddressAttribute().IsValid(memberSave.Email) == false)
- {
- modelState.AddPropertyError(
- new ValidationResult("Invalid email", new[] { "value" }),
- $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
- }
-
- //default provider!
- var membershipProvider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
-
- var validEmail = ValidateUniqueEmail(memberSave, membershipProvider);
- if (validEmail == false)
- {
- modelState.AddPropertyError(
- new ValidationResult("Email address is already in use", new[] { "value" }),
- $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
- }
-
- var validLogin = ValidateUniqueLogin(memberSave, membershipProvider);
- if (validLogin == false)
- {
- modelState.AddPropertyError(
- new ValidationResult("Username is already in use", new[] { "value" }),
- $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
- }
-
- return base.ValidatePropertyData(postedItem, dto, modelState);
- }
-
- ///
- /// This ensures that the internal membership property types are removed from validation before processing the validation
- /// since those properties are actually mapped to real properties of the IMember.
- /// This also validates any posted data for fields that are sensitive.
- ///
- ///
- ///
- ///
- protected override bool ValidateProperties(MemberSave 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);
- }
-
- //if the user doesn't have access to sensitive values, then we need to validate the incoming properties to check
- //if a sensitive value is being submitted.
- if (UmbracoContextAccessor.UmbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false)
- {
- var sensitiveProperties = postedItem.PersistedContent.ContentType
- .PropertyTypes.Where(x => postedItem.PersistedContent.ContentType.IsSensitiveProperty(x.Alias))
- .ToList();
-
- foreach (var sensitiveProperty in sensitiveProperties)
- {
- var prop = propertiesToValidate.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
-
- if (prop != null)
- {
- //this should not happen, this means that there was data posted for a sensitive property that
- //the user doesn't have access to, which means that someone is trying to hack the values.
-
- var message = $"property with alias: {prop.Alias} cannot be posted";
- actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new InvalidOperationException(message));
- return false;
- }
- }
- }
-
- return ValidateProperties(propertiesToValidate, postedItem.PersistedContent.Properties.ToList(), actionContext);
- }
-
- internal bool ValidateUniqueLogin(MemberSave contentItem, MembershipProvider membershipProvider)
- {
- if (contentItem == null) throw new ArgumentNullException(nameof(contentItem));
- if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider));
-
- int totalRecs;
- var existingByName = membershipProvider.FindUsersByName(contentItem.Username.Trim(), 0, int.MaxValue, out totalRecs);
- switch (contentItem.Action)
- {
- case ContentSaveAction.Save:
-
- //ok, we're updating the member, we need to check if they are changing their login and if so, does it exist already ?
- if (contentItem.PersistedContent.Username.InvariantEquals(contentItem.Username.Trim()) == false)
- {
- //they are changing their login name
- if (existingByName.Cast().Select(x => x.UserName)
- .Any(x => x == contentItem.Username.Trim()))
- {
- //the user cannot use this login
- return false;
- }
- }
- break;
- case ContentSaveAction.SaveNew:
- //check if the user's login already exists
- if (existingByName.Cast().Select(x => x.UserName)
- .Any(x => x == contentItem.Username.Trim()))
- {
- //the user cannot use this login
- return false;
- }
- break;
- default:
- //we don't support this for members
- throw new ArgumentOutOfRangeException();
- }
-
- return true;
- }
-
- internal bool ValidateUniqueEmail(MemberSave contentItem, MembershipProvider membershipProvider)
- {
- if (contentItem == null) throw new ArgumentNullException(nameof(contentItem));
- if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider));
-
- if (membershipProvider.RequiresUniqueEmail == false)
- {
- return true;
- }
-
- int totalRecs;
- var existingByEmail = membershipProvider.FindUsersByEmail(contentItem.Email.Trim(), 0, int.MaxValue, out totalRecs);
- switch (contentItem.Action)
- {
- case ContentSaveAction.Save:
- //ok, we're updating the member, we need to check if they are changing their email and if so, does it exist already ?
- if (contentItem.PersistedContent.Email.InvariantEquals(contentItem.Email.Trim()) == false)
- {
- //they are changing their email
- if (existingByEmail.Cast().Select(x => x.Email)
- .Any(x => x.InvariantEquals(contentItem.Email.Trim())))
- {
- //the user cannot use this email
- return false;
- }
- }
- break;
- case ContentSaveAction.SaveNew:
- //check if the user's email already exists
- if (existingByEmail.Cast().Select(x => x.Email)
- .Any(x => x.InvariantEquals(contentItem.Email.Trim())))
- {
- //the user cannot use this email
- return false;
- }
- break;
- default:
- //we don't support this for members
- throw new ArgumentOutOfRangeException();
- }
-
- return true;
- }
- }
- }
-}
diff --git a/src/Umbraco.Web/WebApi/Filters/ValidationFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ValidationFilterAttribute.cs
index cd737646bf..f56d9c28c4 100644
--- a/src/Umbraco.Web/WebApi/Filters/ValidationFilterAttribute.cs
+++ b/src/Umbraco.Web/WebApi/Filters/ValidationFilterAttribute.cs
@@ -8,6 +8,7 @@ using System.Web.Http.Filters;
namespace Umbraco.Web.WebApi.Filters
{
+
///
/// An action filter used to do basic validation against the model and return a result
/// straight away if it fails.
diff --git a/src/Umbraco.Web/Editors/ParameterSwapControllerActionSelector.cs b/src/Umbraco.Web/WebApi/ParameterSwapControllerActionSelector.cs
similarity index 97%
rename from src/Umbraco.Web/Editors/ParameterSwapControllerActionSelector.cs
rename to src/Umbraco.Web/WebApi/ParameterSwapControllerActionSelector.cs
index 03e982c7cd..8145724bcc 100644
--- a/src/Umbraco.Web/Editors/ParameterSwapControllerActionSelector.cs
+++ b/src/Umbraco.Web/WebApi/ParameterSwapControllerActionSelector.cs
@@ -1,18 +1,13 @@
using System;
-using System.Collections;
using System.Linq;
using System.Net.Http;
-using System.Net.Http.Formatting;
using System.Web;
-using System.Web.Http;
using System.Web.Http.Controllers;
-using System.Web.Http.Validation;
-using System.Web.Http.ValueProviders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
-namespace Umbraco.Web.Editors
+namespace Umbraco.Web.WebApi
{
///
/// This is used to auto-select specific actions on controllers that would otherwise be ambiguous based on a single parameter type
diff --git a/src/Umbraco.Web/WebApi/TrimModelBinder.cs b/src/Umbraco.Web/WebApi/TrimModelBinder.cs
new file mode 100644
index 0000000000..0361612bb8
--- /dev/null
+++ b/src/Umbraco.Web/WebApi/TrimModelBinder.cs
@@ -0,0 +1,23 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
+
+namespace Umbraco.Web.WebApi
+{
+ ///
+ /// A model binder to trim the string
+ ///
+ internal class TrimModelBinder : IModelBinder
+ {
+ public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueResult?.AttemptedValue == null)
+ {
+ return false;
+ }
+
+ bindingContext.Model = (string.IsNullOrWhiteSpace(valueResult.AttemptedValue) ? valueResult.AttemptedValue : valueResult.AttemptedValue.Trim());
+ return true;
+ }
+ }
+}