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