From a4a074e22c1b50e438424744e0ea4b993b55a676 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 12 Jun 2020 11:14:06 +0200 Subject: [PATCH] More content model binding Signed-off-by: Bjarke Berg --- .../UmbracoNotificationSuccessResponse.cs | 19 ++ .../Controllers/ContentController.cs | 64 ++-- .../Controllers/ContentControllerBase.cs | 6 +- ...EnsureUserPermissionForContentAttribute.cs | 51 ++- .../FileUploadCleanupFilterAttribute.cs | 163 ++++++++++ .../ModelBinders/BlueprintItemBinder.cs | 7 +- .../ModelBinders/ContentItemBinder.cs | 305 +++++++++++++----- .../ModelBinders/ContentModelBinderHelper.cs | 130 ++++---- .../ModelBinders/MediaItemBinder.cs | 156 ++++----- .../ModelBinders/MemberBinder.cs | 288 ++++++++--------- .../Umbraco.Web.BackOffice.csproj | 1 - .../AspNetCore/AspNetCoreRequestAccessor.cs | 7 +- 12 files changed, 788 insertions(+), 409 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs create mode 100644 src/Umbraco.Web.BackOffice/Filters/FileUploadCleanupFilterAttribute.cs diff --git a/src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs b/src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs new file mode 100644 index 0000000000..c1e0e8c601 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Common.ActionResults +{ + public class UmbracoNotificationSuccessResponse : OkObjectResult + { + public UmbracoNotificationSuccessResponse(string successMessage) : base(null) + { + var notificationModel = new SimpleNotificationModel + { + Message = successMessage + }; + notificationModel.AddSuccessNotification(successMessage, string.Empty); + + Value = notificationModel; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 22b062d5a8..313e197eb6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; @@ -31,8 +30,10 @@ using Constants = Umbraco.Core.Constants; using Umbraco.Extensions; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.ActionResults; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors.Binders; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Security; using Umbraco.Web.WebApi.Filters; @@ -77,7 +78,7 @@ namespace Umbraco.Web.Editors ICultureDictionary cultureDictionary, ILogger logger, IShortStringHelper shortStringHelper, - EventMessages eventMessages, + IEventMessagesFactory eventMessages, ILocalizedTextService localizedTextService, PropertyEditorCollection propertyEditors, IContentService contentService, @@ -118,6 +119,9 @@ namespace Umbraco.Web.Editors _actionCollection = actionCollection; _memberGroupService = memberGroupService; _sqlContext = sqlContext; + + _allLangs = new Lazy>(() => _localizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase)); + } /// @@ -606,32 +610,32 @@ namespace Umbraco.Web.Editors } } - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentSaveValidation] - public ContentItemDisplay PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) - { - var contentItemDisplay = PostSaveInternal(contentItem, - content => - { - EnsureUniqueName(content.Name, content, "Name"); + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentSaveValidation] + public ContentItemDisplay PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, + content => + { + EnsureUniqueName(content.Name, content, "Name"); - _contentService.SaveBlueprint(contentItem.PersistedContent, _webSecurity.CurrentUser.Id); - //we need to reuse the underlying logic so return the result that it wants - return OperationResult.Succeed(new EventMessages()); - }, - content => - { - var display = MapToDisplay(content); - SetupBlueprint(display, content); - return display; - }); + _contentService.SaveBlueprint(contentItem.PersistedContent, _webSecurity.CurrentUser.Id); + //we need to reuse the underlying logic so return the result that it wants + return OperationResult.Succeed(new EventMessages()); + }, + content => + { + var display = MapToDisplay(content); + SetupBlueprint(display, content); + return display; + }); - return contentItemDisplay; - } + return contentItemDisplay; + } /// /// Saves content @@ -1571,11 +1575,11 @@ namespace Umbraco.Web.Editors [HttpDelete] [HttpPost] [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)] - public HttpResponseMessage EmptyRecycleBin() + public IActionResult EmptyRecycleBin() { _contentService.EmptyRecycleBin(_webSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return Request.CreateNotificationSuccessResponse(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return new UmbracoNotificationSuccessResponse(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); } /// @@ -2513,8 +2517,4 @@ namespace Umbraco.Web.Editors : Problem(); } } - - internal class _publishedUrlProvider - { - } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index bf23c1295f..2d7121e2e4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -32,14 +32,14 @@ namespace Umbraco.Web.Editors protected ICultureDictionary CultureDictionary { get; } protected ILogger Logger { get; } protected IShortStringHelper ShortStringHelper { get; } - protected EventMessages EventMessages { get; } + protected IEventMessagesFactory EventMessages { get; } protected ILocalizedTextService LocalizedTextService { get; } protected ContentControllerBase( ICultureDictionary cultureDictionary, ILogger logger, IShortStringHelper shortStringHelper, - EventMessages eventMessages, + IEventMessagesFactory eventMessages, ILocalizedTextService localizedTextService) { CultureDictionary = cultureDictionary; @@ -173,7 +173,7 @@ namespace Umbraco.Web.Editors { //if there's already a default event message, don't add our default one var msgs = EventMessages; - if (msgs != null && msgs.GetAll().Any(x => x.IsDefaultEventMessage)) return; + if (msgs != null && msgs.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) return; display.AddWarningNotification( localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header, diff --git a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs index 398263975d..131854808e 100644 --- a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs @@ -31,7 +31,7 @@ namespace Umbraco.Web.BackOffice.Filters { Arguments = new object[] { - nodeId, null, null + nodeId }; } @@ -41,7 +41,7 @@ namespace Umbraco.Web.BackOffice.Filters { Arguments = new object[] { - nodeId, null, permissionToCheck + nodeId, permissionToCheck }; } @@ -54,7 +54,7 @@ namespace Umbraco.Web.BackOffice.Filters Arguments = new object[] { - null, paramName, ActionBrowse.ActionLetter + paramName, ActionBrowse.ActionLetter }; } @@ -68,7 +68,7 @@ namespace Umbraco.Web.BackOffice.Filters Arguments = new object[] { - null, paramName, permissionToCheck + paramName, permissionToCheck }; } @@ -82,9 +82,52 @@ namespace Umbraco.Web.BackOffice.Filters private readonly string _paramName; private readonly char? _permissionToCheck; + public EnsureUserPermissionForContentFilter( + IWebSecurity webSecurity, + IEntityService entityService, + IUserService userService, + IContentService contentService, + string paramName) + :this(webSecurity, entityService, userService, contentService, null, paramName, ActionBrowse.ActionLetter) + { + } public EnsureUserPermissionForContentFilter( + IWebSecurity webSecurity, + IEntityService entityService, + IUserService userService, + IContentService contentService, + int nodeId, + char permissionToCheck) + :this(webSecurity, entityService, userService, contentService, nodeId, null, permissionToCheck) + { + + } + + public EnsureUserPermissionForContentFilter( + IWebSecurity webSecurity, + IEntityService entityService, + IUserService userService, + IContentService contentService, + int nodeId) + :this(webSecurity, entityService, userService, contentService, nodeId, null, null) + { + + } + public EnsureUserPermissionForContentFilter( + IWebSecurity webSecurity, + IEntityService entityService, + IUserService userService, + IContentService contentService, + string paramName, char permissionToCheck) + :this(webSecurity, entityService, userService, contentService, null, paramName, permissionToCheck) + { + + } + + + private EnsureUserPermissionForContentFilter( IWebSecurity webSecurity, IEntityService entityService, IUserService userService, diff --git a/src/Umbraco.Web.BackOffice/Filters/FileUploadCleanupFilterAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/FileUploadCleanupFilterAttribute.cs new file mode 100644 index 0000000000..0646f94121 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/FileUploadCleanupFilterAttribute.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Checks if the parameter is IHaveUploadedFiles and then deletes any temporary saved files from file uploads + /// associated with the request + /// + internal sealed class FileUploadCleanupFilterAttribute : TypeFilterAttribute + { + /// + /// Constructor specifies if the filter should analyze the incoming or outgoing model + /// + /// + public FileUploadCleanupFilterAttribute( bool incomingModel = true) : base(typeof(FileUploadCleanupFilter)) + { + + Arguments = new object[] + { + incomingModel + }; + } + + // We need to use IAsyncActionFilter even that we dont have any async because we need access to + // context.ActionArguments, and this is only available on ActionExecutingContext and not on + // ActionExecutedContext + + private class FileUploadCleanupFilter : IAsyncActionFilter + { + private readonly ILogger _logger; + private readonly bool _incomingModel; + + public FileUploadCleanupFilter(ILogger logger, bool incomingModel) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _incomingModel = incomingModel; + } + + + public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + next(); // We only to do stuff after the action is executed + + var tempFolders = new List(); + + if (_incomingModel) + { + if (context.ActionArguments.Any()) + { + + if (context.ActionArguments.First().Value is IHaveUploadedFiles contentItem) + { + //cleanup any files associated + foreach (var f in contentItem.UploadedFiles) + { + //track all temp folders so we can remove old files afterwards + var dir = Path.GetDirectoryName(f.TempFilePath); + if (tempFolders.Contains(dir) == false) + { + tempFolders.Add(dir); + } + + try + { + File.Delete(f.TempFilePath); + } + catch (Exception ex) + { + _logger.Error(ex, + "Could not delete temp file {FileName}", f.TempFilePath); + } + } + } + } + } + else + { + if (context == null) + { + _logger.Warn("The context is null!!??"); + return Task.CompletedTask; + } + + if (context.Result == null) + { + _logger.Warn( + "The context.Result is null!!??"); + return Task.CompletedTask; + } + + if(!(context.Result is ObjectResult objectResult)) + { + _logger.Warn( + "Could not acquire context.Result as ObjectResult"); + return Task.CompletedTask; + } + + if (objectResult.Value is IHaveUploadedFiles uploadedFiles) + { + if (uploadedFiles.UploadedFiles != null) + { + //cleanup any files associated + foreach (var f in uploadedFiles.UploadedFiles) + { + if (f.TempFilePath.IsNullOrWhiteSpace() == false) + { + //track all temp folders so we can remove old files afterwards + var dir = Path.GetDirectoryName(f.TempFilePath); + if (tempFolders.Contains(dir) == false) + { + tempFolders.Add(dir); + } + + _logger.Debug( + "Removing temp file {FileName}", f.TempFilePath); + + try + { + File.Delete(f.TempFilePath); + } + catch (Exception ex) + { + _logger.Error(ex, + "Could not delete temp file {FileName}", f.TempFilePath); + } + + //clear out the temp path so it's not returned in the response + f.TempFilePath = ""; + } + else + { + _logger.Warn( + "The f.TempFilePath is null or whitespace!!??"); + } + } + } + else + { + _logger.Warn( + "The uploadedFiles.UploadedFiles is null!!??"); + } + } + else + { + _logger.Warn( + "The actionExecutedContext.Request.Content.Value is not IHaveUploadedFiles, it is {ObjectType}", + objectResult.Value.GetType()); + } + } + return Task.CompletedTask; + } + } + } +} diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs index 6d7b575012..993566e5b1 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs @@ -1,6 +1,7 @@ -using Umbraco.Core.Logging; +using Umbraco.Core.Hosting; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Serialization; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Web.Models.ContentEditing; @@ -11,9 +12,9 @@ namespace Umbraco.Web.Editors.Binders { private readonly ContentService _contentService; - public BlueprintItemBinder(UmbracoMapper umbracoMapper, ContentTypeService contentTypeService, ContentService contentService) : base(umbracoMapper, contentTypeService, contentService) + public BlueprintItemBinder(IJsonSerializer jsonSerializer, UmbracoMapper umbracoMapper, IContentService contentService, IContentTypeService contentTypeService, IHostingEnvironment hostingEnvironment, ContentService contentService2) : base(jsonSerializer, umbracoMapper, contentService, contentTypeService, hostingEnvironment) { - _contentService = contentService; + _contentService = contentService2; } protected override IContent GetExisting(ContentItemSave model) diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs index 7e29e3e9d1..0044a3bd25 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs @@ -1,12 +1,21 @@ using System; +using System.Drawing.Drawing2D; +using System.IO; using System.Linq; +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Umbraco.Core; +using Umbraco.Core.Hosting; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.Serialization; +using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Extensions; +using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; @@ -17,91 +26,112 @@ namespace Umbraco.Web.Editors.Binders /// internal class ContentItemBinder : IModelBinder { + private readonly IJsonSerializer _jsonSerializer; private readonly UmbracoMapper _umbracoMapper; - private readonly ContentTypeService _contentTypeService; - private readonly ContentService _contentService; - private readonly ContentModelBinderHelper _modelBinderHelper; + private readonly IContentService _contentService; + private readonly IContentTypeService _contentTypeService; + private readonly IHostingEnvironment _hostingEnvironment; + private ContentModelBinderHelper _modelBinderHelper; - public ContentItemBinder(UmbracoMapper umbracoMapper, ContentTypeService contentTypeService, ContentService contentService) + public ContentItemBinder( + IJsonSerializer jsonSerializer, + UmbracoMapper umbracoMapper, + IContentService contentService, + IContentTypeService contentTypeService, + IHostingEnvironment hostingEnvironment) { + _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); - _contentTypeService = contentTypeService ?? throw new ArgumentNullException(nameof(contentTypeService)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + _contentTypeService = contentTypeService ?? throw new ArgumentNullException(nameof(contentTypeService)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); _modelBinderHelper = new ContentModelBinderHelper(); } - - /// - /// Creates the model from the request and binds it to the context - /// - /// - /// - /// - public Task BindModelAsync(ModelBindingContext bindingContext) - { - var actionContext = bindingContext.ActionContext; - - var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); - if (model == null) - { - bindingContext.Result = ModelBindingResult.Failed(); - return Task.CompletedTask; - } - - model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); - - //create the dto from the persisted model - if (model.PersistedContent != null) - { - foreach (var variant in model.Variants) - { - //map the property dto collection with the culture of the current variant - variant.PropertyCollectionDto = _umbracoMapper.Map( - model.PersistedContent, - context => - { - // either of these may be null and that is ok, if it's invariant they will be null which is what is expected - context.SetCulture(variant.Culture); - context.SetSegment(variant.Segment); - }); - - //now map all of the saved values to the dto - _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); - } - } - - return Task.CompletedTask; - } - - public bool BindModel(ActionContext actionContext, ModelBindingContext bindingContext) - { - var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); - if (model == null) return false; - - model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); - - //create the dto from the persisted model - if (model.PersistedContent != null) - { - foreach (var variant in model.Variants) - { - //map the property dto collection with the culture of the current variant - variant.PropertyCollectionDto = _umbracoMapper.Map( - model.PersistedContent, - context => - { - // either of these may be null and that is ok, if it's invariant they will be null which is what is expected - context.SetCulture(variant.Culture); - context.SetSegment(variant.Segment); - }); - - //now map all of the saved values to the dto - _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); - } - } - - return true; - } - + // private readonly UmbracoMapper _umbracoMapper; + // private readonly ContentTypeService _contentTypeService; + // private readonly ContentService _contentService; + // private readonly ContentModelBinderHelper _modelBinderHelper; + // + // public ContentItemBinder(UmbracoMapper umbracoMapper, ContentTypeService contentTypeService, ContentService contentService) + // { + // _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + // _contentTypeService = contentTypeService ?? throw new ArgumentNullException(nameof(contentTypeService)); + // _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + // _modelBinderHelper = new ContentModelBinderHelper(); + // } + // + // /// + // /// Creates the model from the request and binds it to the context + // /// + // /// + // /// + // /// + // public Task BindModelAsync(ModelBindingContext bindingContext) + // { + // var actionContext = bindingContext.ActionContext; + // + // var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); + // if (model == null) + // { + // bindingContext.Result = ModelBindingResult.Failed(); + // return Task.CompletedTask; + // } + // + // model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + // + // //create the dto from the persisted model + // if (model.PersistedContent != null) + // { + // foreach (var variant in model.Variants) + // { + // //map the property dto collection with the culture of the current variant + // variant.PropertyCollectionDto = _umbracoMapper.Map( + // model.PersistedContent, + // context => + // { + // // either of these may be null and that is ok, if it's invariant they will be null which is what is expected + // context.SetCulture(variant.Culture); + // context.SetSegment(variant.Segment); + // }); + // + // //now map all of the saved values to the dto + // _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); + // } + // } + // + // return Task.CompletedTask; + // } + // + // public bool BindModel(ActionContext actionContext, ModelBindingContext bindingContext) + // { + // var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); + // if (model == null) return false; + // + // model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + // + // //create the dto from the persisted model + // if (model.PersistedContent != null) + // { + // foreach (var variant in model.Variants) + // { + // //map the property dto collection with the culture of the current variant + // variant.PropertyCollectionDto = _umbracoMapper.Map( + // model.PersistedContent, + // context => + // { + // // either of these may be null and that is ok, if it's invariant they will be null which is what is expected + // context.SetCulture(variant.Culture); + // context.SetSegment(variant.Segment); + // }); + // + // //now map all of the saved values to the dto + // _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); + // } + // } + // + // return true; + // } + // protected virtual IContent GetExisting(ContentItemSave model) { return _contentService.GetById(model.Id); @@ -121,5 +151,124 @@ namespace Umbraco.Web.Editors.Binders } + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException(nameof(bindingContext)); + } + var modelName = bindingContext.ModelName; + + var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + + if (valueProviderResult == ValueProviderResult.None) + { + return; + } + bindingContext.ModelState.SetModelValue(modelName, valueProviderResult); + + var value = valueProviderResult.FirstValue; + + // Check if the argument value is null or empty + if (string.IsNullOrEmpty(value)) + { + return; + } + var model = _jsonSerializer.Deserialize(value); + if (model is null) + { + // Non-integer arguments result in model state errors + bindingContext.ModelState.TryAddModelError( + modelName, $"Cannot deserialize {modelName} as {nameof(ContentItemSave)}."); + + return; + } + + //Handle file uploads + foreach (var formFile in bindingContext.HttpContext.Request.Form.Files) + { + //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id + // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. + var parts = formFile.Name.Trim('\"').Split('_'); + if (parts.Length < 2) + { + bindingContext.HttpContext.SetReasonPhrase( "The request was not formatted correctly the file name's must be underscore delimited"); + throw new HttpResponseException(HttpStatusCode.BadRequest); + } + var propAlias = parts[1]; + + //if there are 3 parts part 3 is always culture + string culture = null; + if (parts.Length > 2) + { + culture = parts[2]; + //normalize to null if empty + if (culture.IsNullOrWhiteSpace()) + { + culture = null; + } + } + + //if there are 4 parts part 4 is always segment + string segment = null; + if (parts.Length > 3) + { + segment = parts[3]; + //normalize to null if empty + if (segment.IsNullOrWhiteSpace()) + { + segment = null; + } + } + + // TODO: anything after 4 parts we can put in metadata + + var fileName = formFile.FileName.Trim('\"'); + + var tempFileUploadFolder = _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempFileUploads); + Directory.CreateDirectory(tempFileUploadFolder); + var tempFilePath = Path.Combine(tempFileUploadFolder, Guid.NewGuid().ToString()); + + using (var stream = System.IO.File.Create(tempFilePath)) + { + await formFile.CopyToAsync(stream); + } + + model.UploadedFiles.Add(new ContentPropertyFile + { + TempFilePath = tempFilePath, + PropertyAlias = propAlias, + Culture = culture, + Segment = segment, + FileName = fileName + }); + } + + + + model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + + //create the dto from the persisted model + if (model.PersistedContent != null) + { + foreach (var variant in model.Variants) + { + //map the property dto collection with the culture of the current variant + variant.PropertyCollectionDto = _umbracoMapper.Map( + model.PersistedContent, + context => + { + // either of these may be null and that is ok, if it's invariant they will be null which is what is expected + context.SetCulture(variant.Culture); + context.SetSegment(variant.Segment); + }); + + //now map all of the saved values to the dto + _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); + } + } + + bindingContext.Result = ModelBindingResult.Success(model); + } } } diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs index ab980852e0..21873f5a12 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs @@ -17,71 +17,71 @@ namespace Umbraco.Web.Editors.Binders /// internal class ContentModelBinderHelper { - public TModelSave BindModelFromMultipartRequest(ActionContext actionContext, - ModelBindingContext bindingContext) - where TModelSave : IHaveUploadedFiles - { - var result = actionContext.ReadAsMultipart(Constants.SystemDirectories.TempFileUploads); - - var model = actionContext.GetModelFromMultipartRequest(result, "contentItem"); - - //get the files - foreach (var file in result.FileData) - { - //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id - // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. - var parts = file.Headers.ContentDisposition.Name.Trim('\"').Split('_'); - if (parts.Length < 2) - { - bindingContext.HttpContext.SetReasonPhrase( - "The request was not formatted correctly the file name's must be underscore delimited"); - throw new HttpResponseException(HttpStatusCode.BadRequest); - } - - var propAlias = parts[1]; - - //if there are 3 parts part 3 is always culture - string culture = null; - if (parts.Length > 2) - { - culture = parts[2]; - //normalize to null if empty - if (culture.IsNullOrWhiteSpace()) - { - culture = null; - } - } - - //if there are 4 parts part 4 is always segment - string segment = null; - if (parts.Length > 3) - { - segment = parts[3]; - //normalize to null if empty - if (segment.IsNullOrWhiteSpace()) - { - segment = null; - } - } - - // TODO: anything after 4 parts we can put in metadata - - var fileName = file.Headers.ContentDisposition.FileName.Trim('\"'); - - model.UploadedFiles.Add(new ContentPropertyFile - { - TempFilePath = file.LocalFileName, - PropertyAlias = propAlias, - Culture = culture, - Segment = segment, - FileName = fileName - }); - } - - bindingContext.Model = model; - - return model; - } + // public TModelSave BindModelFromMultipartRequest(ActionContext actionContext, + // ModelBindingContext bindingContext) + // where TModelSave : IHaveUploadedFiles + // { + // var result = actionContext.ReadAsMultipart(Constants.SystemDirectories.TempFileUploads); + // + // var model = actionContext.GetModelFromMultipartRequest(result, "contentItem"); + // + // //get the files + // foreach (var file in result.FileData) + // { + // //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id + // // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. + // var parts = file.Headers.ContentDisposition.Name.Trim('\"').Split('_'); + // if (parts.Length < 2) + // { + // bindingContext.HttpContext.SetReasonPhrase( + // "The request was not formatted correctly the file name's must be underscore delimited"); + // throw new HttpResponseException(HttpStatusCode.BadRequest); + // } + // + // var propAlias = parts[1]; + // + // //if there are 3 parts part 3 is always culture + // string culture = null; + // if (parts.Length > 2) + // { + // culture = parts[2]; + // //normalize to null if empty + // if (culture.IsNullOrWhiteSpace()) + // { + // culture = null; + // } + // } + // + // //if there are 4 parts part 4 is always segment + // string segment = null; + // if (parts.Length > 3) + // { + // segment = parts[3]; + // //normalize to null if empty + // if (segment.IsNullOrWhiteSpace()) + // { + // segment = null; + // } + // } + // + // // TODO: anything after 4 parts we can put in metadata + // + // var fileName = file.Headers.ContentDisposition.FileName.Trim('\"'); + // + // model.UploadedFiles.Add(new ContentPropertyFile + // { + // TempFilePath = file.LocalFileName, + // PropertyAlias = propAlias, + // Culture = culture, + // Segment = segment, + // FileName = fileName + // }); + // } + // + // bindingContext.Model = model; + // + // return model; + // } /// /// we will now assign all of the values in the 'save' model to the DTO object diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs index 86272eb12f..a617d03625 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs @@ -1,78 +1,78 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Umbraco.Core.Mapping; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.Editors.Binders -{ - /// - /// The model binder for - /// - internal class MediaItemBinder : IModelBinder - { - private readonly IMediaService _mediaService; - private readonly UmbracoMapper _umbracoMapper; - private readonly IMediaTypeService _mediaTypeService; - private readonly ContentModelBinderHelper _modelBinderHelper; - - - public MediaItemBinder(IMediaService mediaService, UmbracoMapper umbracoMapper, IMediaTypeService mediaTypeService) - { - _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); - _mediaTypeService = mediaTypeService ?? throw new ArgumentNullException(nameof(mediaTypeService)); - - _modelBinderHelper = new ContentModelBinderHelper(); - } - - /// - /// Creates the model from the request and binds it to the context - /// - /// - /// - public Task BindModelAsync(ModelBindingContext bindingContext) - { - var actionContext = bindingContext.ActionContext; - var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); - if (model == null) - { - bindingContext.Result = ModelBindingResult.Failed(); - return Task.CompletedTask; - } - - model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); - - //create the dto from the persisted model - if (model.PersistedContent != null) - { - model.PropertyCollectionDto = _umbracoMapper.Map(model.PersistedContent); - //now map all of the saved values to the dto - _modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); - } - - model.Name = model.Name.Trim(); - - bindingContext.Result = ModelBindingResult.Success(model); - return Task.CompletedTask; - } - - private IMedia GetExisting(MediaItemSave model) - { - return _mediaService.GetById(Convert.ToInt32(model.Id)); - } - - private IMedia CreateNew(MediaItemSave model) - { - var mediaType = _mediaTypeService.Get(model.ContentTypeAlias); - if (mediaType == null) - { - throw new InvalidOperationException("No media type found with alias " + model.ContentTypeAlias); - } - return new Core.Models.Media(model.Name, model.ParentId, mediaType); - } - - } -} +// using System; +// using System.Threading.Tasks; +// using Microsoft.AspNetCore.Mvc.ModelBinding; +// using Umbraco.Core.Mapping; +// using Umbraco.Core.Models; +// using Umbraco.Core.Services; +// using Umbraco.Web.Models.ContentEditing; +// +// namespace Umbraco.Web.Editors.Binders +// { +// /// +// /// The model binder for +// /// +// internal class MediaItemBinder : IModelBinder +// { +// private readonly IMediaService _mediaService; +// private readonly UmbracoMapper _umbracoMapper; +// private readonly IMediaTypeService _mediaTypeService; +// private readonly ContentModelBinderHelper _modelBinderHelper; +// +// +// public MediaItemBinder(IMediaService mediaService, UmbracoMapper umbracoMapper, IMediaTypeService mediaTypeService) +// { +// _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); +// _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); +// _mediaTypeService = mediaTypeService ?? throw new ArgumentNullException(nameof(mediaTypeService)); +// +// _modelBinderHelper = new ContentModelBinderHelper(); +// } +// +// /// +// /// Creates the model from the request and binds it to the context +// /// +// /// +// /// +// public Task BindModelAsync(ModelBindingContext bindingContext) +// { +// var actionContext = bindingContext.ActionContext; +// var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); +// if (model == null) +// { +// bindingContext.Result = ModelBindingResult.Failed(); +// return Task.CompletedTask; +// } +// +// model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); +// +// //create the dto from the persisted model +// if (model.PersistedContent != null) +// { +// model.PropertyCollectionDto = _umbracoMapper.Map(model.PersistedContent); +// //now map all of the saved values to the dto +// _modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); +// } +// +// model.Name = model.Name.Trim(); +// +// bindingContext.Result = ModelBindingResult.Success(model); +// return Task.CompletedTask; +// } +// +// private IMedia GetExisting(MediaItemSave model) +// { +// return _mediaService.GetById(Convert.ToInt32(model.Id)); +// } +// +// private IMedia CreateNew(MediaItemSave model) +// { +// var mediaType = _mediaTypeService.Get(model.ContentTypeAlias); +// if (mediaType == null) +// { +// throw new InvalidOperationException("No media type found with alias " + model.ContentTypeAlias); +// } +// return new Core.Models.Media(model.Name, model.ParentId, mediaType); +// } +// +// } +// } diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs index 912fcc6c03..8e857a687f 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs @@ -1,144 +1,144 @@ -using System; -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Web.Models.ContentEditing; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Umbraco.Core.Mapping; - -namespace Umbraco.Web.Editors.Binders -{ - /// - /// The model binder for - /// - internal class MemberBinder : IModelBinder - { - private readonly ContentModelBinderHelper _modelBinderHelper; - private readonly IShortStringHelper _shortStringHelper; - private readonly UmbracoMapper _umbracoMapper; - private readonly IMemberService _memberService; - private readonly IMemberTypeService _memberTypeService; - - public MemberBinder( - IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, - IMemberService memberService, - IMemberTypeService memberTypeService) - { - - _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); - _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); - _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); - _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); - _modelBinderHelper = new ContentModelBinderHelper(); - } - - /// - /// Creates the model from the request and binds it to the context - /// - /// - /// - /// - public Task BindModelAsync(ModelBindingContext bindingContext) - { - var actionContext = bindingContext.ActionContext; - var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); - if (model == null) - { - bindingContext.Result = ModelBindingResult.Failed(); - return Task.CompletedTask; - } - - model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); - - //create the dto from the persisted model - if (model.PersistedContent != null) - { - model.PropertyCollectionDto = _umbracoMapper.Map(model.PersistedContent); - //now map all of the saved values to the dto - _modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); - } - - model.Name = model.Name.Trim(); - - bindingContext.Result = ModelBindingResult.Success(model); - return Task.CompletedTask; - } - - /// - /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) - /// - /// - /// - private IMember GetExisting(MemberSave model) - { - return GetExisting(model.Key); - } - - private IMember GetExisting(Guid key) - { - var member = _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. - /// - private IMember CreateNew(MemberSave model) - { - var contentType = _memberTypeService.Get(model.ContentTypeAlias); - if (contentType == null) - { - throw new InvalidOperationException("No member type found with 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); - } - - /// - /// 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 to 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 = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - //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); - } - } - } - - - } -} +// using System; +// using System.Collections.Generic; +// using Umbraco.Core; +// using Umbraco.Core.Models; +// using Umbraco.Core.Services; +// using Umbraco.Core.Strings; +// using Umbraco.Web.Models.ContentEditing; +// using System.Linq; +// using System.Threading.Tasks; +// using Microsoft.AspNetCore.Mvc.ModelBinding; +// using Umbraco.Core.Mapping; +// +// namespace Umbraco.Web.Editors.Binders +// { +// /// +// /// The model binder for +// /// +// internal class MemberBinder : IModelBinder +// { +// private readonly ContentModelBinderHelper _modelBinderHelper; +// private readonly IShortStringHelper _shortStringHelper; +// private readonly UmbracoMapper _umbracoMapper; +// private readonly IMemberService _memberService; +// private readonly IMemberTypeService _memberTypeService; +// +// public MemberBinder( +// IShortStringHelper shortStringHelper, +// UmbracoMapper umbracoMapper, +// IMemberService memberService, +// IMemberTypeService memberTypeService) +// { +// +// _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); +// _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); +// _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); +// _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); +// _modelBinderHelper = new ContentModelBinderHelper(); +// } +// +// /// +// /// Creates the model from the request and binds it to the context +// /// +// /// +// /// +// /// +// public Task BindModelAsync(ModelBindingContext bindingContext) +// { +// var actionContext = bindingContext.ActionContext; +// var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); +// if (model == null) +// { +// bindingContext.Result = ModelBindingResult.Failed(); +// return Task.CompletedTask; +// } +// +// model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); +// +// //create the dto from the persisted model +// if (model.PersistedContent != null) +// { +// model.PropertyCollectionDto = _umbracoMapper.Map(model.PersistedContent); +// //now map all of the saved values to the dto +// _modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); +// } +// +// model.Name = model.Name.Trim(); +// +// bindingContext.Result = ModelBindingResult.Success(model); +// return Task.CompletedTask; +// } +// +// /// +// /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) +// /// +// /// +// /// +// private IMember GetExisting(MemberSave model) +// { +// return GetExisting(model.Key); +// } +// +// private IMember GetExisting(Guid key) +// { +// var member = _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. +// /// +// private IMember CreateNew(MemberSave model) +// { +// var contentType = _memberTypeService.Get(model.ContentTypeAlias); +// if (contentType == null) +// { +// throw new InvalidOperationException("No member type found with 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); +// } +// +// /// +// /// 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 to 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 = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); +// //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); +// } +// } +// } +// +// +// } +// } diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index ed2b2b482c..8bb44b5f34 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -27,5 +27,4 @@ - diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs index 33b99b3d5f..d29e58a2bf 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs @@ -44,7 +44,12 @@ namespace Umbraco.Web.Common.AspNetCore public string GetRequestValue(string name) => GetFormValue(name) ?? GetQueryStringValue(name); - public string GetFormValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Form[name]; + public string GetFormValue(string name) + { + var request = _httpContextAccessor.GetRequiredHttpContext().Request; + if (!request.HasFormContentType) return null; + return request.Form[name]; + } public string GetQueryStringValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Query[name];