diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/ModelStateExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/ModelStateExtensionsTests.cs index 3c4b78b4fa..dd2c4cd424 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/ModelStateExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/ModelStateExtensionsTests.cs @@ -9,6 +9,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.Extensions; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions { diff --git a/src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs b/src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs deleted file mode 100644 index 37e4902626..0000000000 --- a/src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Web.BackOffice.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/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index c552c0d976..66c0c4b849 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -209,7 +209,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers else { AddModelErrors(result); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return new ValidationErrorResult(ModelState); } } @@ -474,7 +474,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return new ValidationErrorResult(ModelState); } var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs index 4cecb20aa5..27be8ec263 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs @@ -1,4 +1,10 @@ -using Umbraco.Cms.Web.BackOffice.Filters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Web.BackOffice.ActionResults; +using Umbraco.Cms.Web.BackOffice.Filters; +using Umbraco.Cms.Web.Common.ActionsResults; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -11,5 +17,55 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [AppendCurrentEventMessages] public abstract class BackOfficeNotificationsController : UmbracoAuthorizedJsonController { + /// + /// returns a 200 OK response with a notification message + /// + /// + /// + protected OkObjectResult Ok(string message) + { + var notificationModel = new SimpleNotificationModel + { + Message = message + }; + notificationModel.AddSuccessNotification(message, string.Empty); + + return new OkObjectResult(notificationModel); + } + + /// + /// Overridden to ensure that the error message is an error notification message + /// + /// + /// + protected override ActionResult ValidationProblem(string errorMessage) + => ValidationProblem(errorMessage, string.Empty); + + /// + /// Creates a notofication validation problem with a header and message + /// + /// + /// + /// + protected ActionResult ValidationProblem(string errorHeader, string errorMessage) + { + var notificationModel = new SimpleNotificationModel + { + Message = errorMessage + }; + notificationModel.AddErrorNotification(errorHeader, errorMessage); + return new ValidationErrorResult(notificationModel); + } + + /// + /// Overridden to ensure that all queued notifications are sent to the back office + /// + /// + [NonAction] + public override ActionResult ValidationProblem() + // returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + => new ValidationErrorResult(new SimpleNotificationModel()); + } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index 822f5a4911..15a0469a4a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -86,9 +86,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers view.Content = display.Content; var result = _fileService.CreatePartialView(view, display.Snippet, currentUser.Id); if (result.Success) + { return Ok(); + } else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + { + return ValidationProblem(result.Exception.Message); + } case Constants.Trees.PartialViewMacros: var viewMacro = new PartialView(PartialViewType.PartialViewMacro, display.VirtualPath); @@ -97,7 +101,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (resultMacro.Success) return Ok(); else - return ValidationErrorResult.CreateNotificationValidationErrorResult(resultMacro.Exception.Message); + return ValidationProblem(resultMacro.Exception.Message); case Constants.Trees.Scripts: var script = new Script(display.VirtualPath); @@ -123,7 +127,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("Value cannot be null or whitespace.", "parentId"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); if (name.ContainsAny(Path.GetInvalidPathChars())) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(_localizedTextService.Localize("codefile/createFolderIllegalChars")); + return ValidationProblem(_localizedTextService.Localize("codefile/createFolderIllegalChars")); } // if the parentId is root (-1) then we just need an empty string as we are diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index dff4cf54bf..e82c22680d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -607,7 +607,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!EnsureUniqueName(name, content, nameof(name))) { - return new ValidationErrorResult(ModelState.ToErrorDictionary()); + return ValidationProblem(ModelState); } var blueprint = _contentService.CreateContentFromBlueprint(content, name, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); @@ -714,8 +714,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the model state to the outgoing object and throw a validation message var forDisplay = mapToDisplay(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(forDisplay); + return ValidationProblem(forDisplay, ModelState); } // if there's only one variant and the model state is not valid we cannot publish so change it to save @@ -866,8 +865,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //lastly, if it is not valid, add the model state to the outgoing object and throw a 400 if (!ModelState.IsValid) { - display.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(display); + return ValidationProblem(display, ModelState); } if (wasCancelled) @@ -878,7 +876,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //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! - return new ValidationErrorResult(display); + return ValidationProblem(display); } } @@ -1508,7 +1506,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var notificationModel = new SimpleNotificationModel(); AddMessageForPublishStatus(new[] { publishResult }, notificationModel); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(notificationModel); } return Ok(); @@ -1558,9 +1556,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var moveResult = _contentService.MoveToRecycleBin(foundContent, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); if (moveResult.Success == false) { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } } else @@ -1568,9 +1564,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var deleteResult = _contentService.Delete(foundContent, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); if (deleteResult.Success == false) { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } } @@ -1591,7 +1585,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { _contentService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return new UmbracoNotificationSuccessResponse(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Ok(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); } /// @@ -1628,7 +1622,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { _logger.LogWarning("Content sorting failed, this was probably caused by an event being cancelled"); // TODO: Now you can cancel sorting, does the event messages bubble up automatically? - return new ValidationErrorResult("Content sorting failed, this was probably caused by an event being cancelled"); + return ValidationProblem("Content sorting failed, this was probably caused by an event being cancelled"); } return Ok(); @@ -1727,7 +1721,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!unpublishResult.Success) { AddCancelMessage(content); - return new ValidationErrorResult(content); + return ValidationProblem(content); } else { @@ -1799,7 +1793,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } catch (UriFormatException) { - return new ValidationErrorResult(_localizedTextService.Localize("assignDomain/invalidDomain")); + return ValidationProblem(_localizedTextService.Localize("assignDomain/invalidDomain")); } } @@ -2069,7 +2063,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //cannot move if the content item is not allowed at the root if (toMove.ContentType.AllowedAsRoot == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult( + return ValidationProblem( _localizedTextService.Localize("moveOrCopy/notAllowedAtRoot")); } } @@ -2086,14 +2080,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (parentContentType.AllowedContentTypes.Select(x => x.Id).ToArray() .Any(x => x.Value == toMove.ContentType.Id) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult( + return ValidationProblem( _localizedTextService.Localize("moveOrCopy/notAllowedByContentType")); } // Check on paths if ($",{parent.Path},".IndexOf($",{toMove.Id},", StringComparison.Ordinal) > -1) { - return ValidationErrorResult.CreateNotificationValidationErrorResult( + return ValidationProblem( _localizedTextService.Localize("moveOrCopy/notAllowedByPath")); } } @@ -2403,8 +2397,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (rollbackResult.Success) return Ok(); - var notificationModel = new SimpleNotificationModel(); - switch (rollbackResult.Result) { case OperationResultType.Failed: @@ -2412,22 +2404,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case OperationResultType.FailedExceptionThrown: case OperationResultType.NoOperation: default: - notificationModel.AddErrorNotification( - _localizedTextService.Localize("speechBubbles/operationFailedHeader"), - null); // TODO: There is no specific failed to save error message AFAIK - break; + return ValidationProblem(_localizedTextService.Localize("speechBubbles/operationFailedHeader")); case OperationResultType.FailedCancelledByEvent: - notificationModel.AddErrorNotification( - _localizedTextService.Localize("speechBubbles/operationCancelledHeader"), - _localizedTextService.Localize("speechBubbles/operationCancelledText")); - break; + return ValidationProblem( + _localizedTextService.Localize("speechBubbles/operationCancelledHeader"), + _localizedTextService.Localize("speechBubbles/operationCancelledText")); } - - return new ValidationErrorResult(notificationModel); } - - - - } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index 79ea6f6329..95e9b5ecfe 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -297,7 +297,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] @@ -308,7 +308,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs index 46c75e5186..ab7788e139 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs @@ -13,9 +13,7 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -27,7 +25,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [PrefixlessBodyModelValidator] - public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController + public abstract class ContentTypeControllerBase : BackOfficeNotificationsController where TContentType : class, IContentTypeComposition { private readonly EditorValidatorCollection _editorValidatorCollection; @@ -283,7 +281,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { var err = CreateModelStateValidationEror(ctId, contentTypeSave, ct); - return new ValidationErrorResult(err); + return ValidationProblem(err); } //filter out empty properties @@ -305,11 +303,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { var responseEx = CreateInvalidCompositionResponseException(ex, contentTypeSave, ct, ctId); - if (responseEx != null) return new ValidationErrorResult(responseEx); + if (responseEx != null) return ValidationProblem(responseEx); } var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, ct); - if (exResult != null) return new ValidationErrorResult(exResult); + if (exResult != null) return ValidationProblem(exResult); saveContentType(ct); @@ -348,11 +346,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (responseEx is null) throw ex; - return new ValidationErrorResult(responseEx); + return ValidationProblem(responseEx); } var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, newCt); - if (exResult != null) return new ValidationErrorResult(exResult); + if (exResult != null) return ValidationProblem(exResult); //set id to null to ensure its handled as a new type contentTypeSave.Id = null; @@ -417,13 +415,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case MoveOperationStatusType.FailedParentNotFound: return NotFound(); case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); case MoveOperationStatusType.FailedNotAllowedByPath: - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(LocalizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(LocalizedTextService.Localize("moveOrCopy/notAllowedByPath")); default: throw new ArgumentOutOfRangeException(); } @@ -458,13 +452,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case MoveOperationStatusType.FailedParentNotFound: return NotFound(); case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); - case MoveOperationStatusType.FailedNotAllowedByPath: - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(LocalizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(); + case MoveOperationStatusType.FailedNotAllowedByPath: + return ValidationProblem(LocalizedTextService.Localize("moveOrCopy/notAllowedByPath")); default: throw new ArgumentOutOfRangeException(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 49bff529bd..d6ebe946bf 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -190,7 +190,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // so that is why it is being used here. ModelState.AddModelError("value", result.Errors.ToErrorMessage()); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } //They've successfully set their password, we can now update their user account to be approved @@ -242,7 +242,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); } - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } // TODO: Why is this necessary? This inherits from UmbracoAuthorizedApiController diff --git a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index 9f020125bb..c29b17f3a3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -16,9 +16,7 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -267,7 +265,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } /// @@ -302,7 +300,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (DuplicateNameException ex) { ModelState.AddModelError("Name", ex.Message); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } // map back to display model, and return @@ -335,13 +333,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case MoveOperationStatusType.FailedParentNotFound: return NotFound(); case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); case MoveOperationStatusType.FailedNotAllowedByPath: var notificationModel = new SimpleNotificationModel(); notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(notificationModel); default: throw new ArgumentOutOfRangeException(); } @@ -355,7 +351,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 980d08c2e2..b00fa20141 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -13,8 +13,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.BackOffice.Extensions; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -101,7 +99,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult Create(int parentId, string key) { if (string.IsNullOrEmpty(key)) - return ValidationErrorResult.CreateNotificationValidationErrorResult("Key can not be empty."); // TODO: translate + return ValidationProblem("Key can not be empty."); // TODO: translate if (_localizationService.DictionaryItemExists(key)) { @@ -109,7 +107,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers "dictionaryItem/changeKeyError", _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserCulture(_localizedTextService, _globalSettings), new Dictionary { { "0", key } }); - return ValidationErrorResult.CreateNotificationValidationErrorResult(message); + return ValidationProblem(message); } try @@ -130,7 +128,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { _logger.LogError(ex, "Error creating dictionary with {Name} under {ParentId}", key, parentId); - return ValidationErrorResult.CreateNotificationValidationErrorResult("Error creating dictionary item"); + return ValidationProblem("Error creating dictionary item"); } } @@ -207,7 +205,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _localizationService.GetDictionaryItemById(int.Parse(dictionary.Id.ToString())); if (dictionaryItem == null) - return ValidationErrorResult.CreateNotificationValidationErrorResult("Dictionary item does not exist"); + return ValidationProblem("Dictionary item does not exist"); var userCulture = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserCulture(_localizedTextService, _globalSettings); @@ -224,7 +222,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers userCulture, new Dictionary { { "0", dictionary.Name } }); ModelState.AddModelError("Name", message); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } dictionaryItem.ItemKey = dictionary.Name; @@ -251,7 +249,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { _logger.LogError(ex, "Error saving dictionary with {Name} under {ParentId}", dictionary.Name, dictionary.ParentId); - return ValidationErrorResult.CreateNotificationValidationErrorResult("Something went wrong saving dictionary"); + return ValidationProblem("Something went wrong saving dictionary"); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 8465e7a454..2cc35c8dfa 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -97,7 +97,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (language.IsDefault) { var message = $"Language '{language.IsoCode}' is currently set to 'default' and can not be deleted."; - return ValidationErrorResult.CreateNotificationValidationErrorResult(message); + return ValidationProblem(message); } // service is happy deleting a language that's fallback for another language, @@ -116,7 +116,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult SaveLanguage(Language language) { if (!ModelState.IsValid) - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); // this is prone to race conditions but the service will not let us proceed anyways var existingByCulture = _localizationService.GetLanguageByIsoCode(language.IsoCode); @@ -132,7 +132,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //someone is trying to create a language that already exist ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } var existingById = language.Id != default ? _localizationService.GetLanguageById(language.Id) : null; @@ -149,7 +149,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (CultureNotFoundException) { ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } // create it (creating a new language cannot create a fallback cycle) @@ -172,7 +172,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (existingById.IsDefault && !language.IsDefault) { ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } existingById.IsDefault = language.IsDefault; @@ -187,12 +187,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!languages.ContainsKey(existingById.FallbackLanguageId.Value)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } if (CreatesCycle(existingById, languages)) { ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path."); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs index 20d33bd83a..9fcf407581 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging.Viewer; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Constants = Umbraco.Cms.Core.Constants; @@ -18,7 +17,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] - public class LogViewerController : UmbracoAuthorizedJsonController + public class LogViewerController : BackOfficeNotificationsController { private readonly ILogViewer _logViewer; @@ -51,7 +50,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Unable to view logs, due to size"); + return ValidationProblem("Unable to view logs, due to size"); } return _logViewer.GetNumberOfErrors(logTimePeriod); @@ -64,7 +63,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Unable to view logs, due to size"); + return ValidationProblem("Unable to view logs, due to size"); } return _logViewer.GetLogLevelCounts(logTimePeriod); @@ -77,7 +76,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Unable to view logs, due to size"); + return ValidationProblem("Unable to view logs, due to size"); } return new ActionResult>(_logViewer.GetMessageTemplates(logTimePeriod)); @@ -91,7 +90,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Unable to view logs, due to size"); + return ValidationProblem("Unable to view logs, due to size"); } var direction = orderDirection == "Descending" ? Direction.Descending : Direction.Ascending; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs index a7ec619ae1..ec91d76c8e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs @@ -15,7 +15,6 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -74,19 +73,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (string.IsNullOrWhiteSpace(name)) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Name can not be empty"); + return ValidationProblem("Name can not be empty"); } var alias = name.ToSafeAlias(_shortStringHelper); if (_macroService.GetByAlias(alias) != null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Macro with this alias already exists"); + return ValidationProblem("Macro with this alias already exists"); } if (name == null || name.Length > 255) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Name cannnot be more than 255 characters in length."); + return ValidationProblem("Name cannnot be more than 255 characters in length."); } try @@ -106,7 +105,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { const string errorMessage = "Error creating macro"; _logger.LogError(exception, errorMessage); - return ValidationErrorResult.CreateNotificationValidationErrorResult(errorMessage); + return ValidationProblem(errorMessage); } } @@ -117,7 +116,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); @@ -132,7 +131,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); @@ -145,12 +144,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var guidUdi = id as GuidUdi; if (guidUdi == null) - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); var macro = _macroService.GetById(guidUdi.Guid); if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); @@ -165,7 +164,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); } _macroService.Delete(macro); @@ -178,19 +177,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (macroDisplay == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("No macro data found in request"); + return ValidationProblem("No macro data found in request"); } if (macroDisplay.Name == null || macroDisplay.Name.Length > 255) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Name cannnot be more than 255 characters in length."); + return ValidationProblem("Name cannnot be more than 255 characters in length."); } var macro = _macroService.GetById(int.Parse(macroDisplay.Id.ToString())); if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {macroDisplay.Id} does not exist"); + return ValidationProblem($"Macro with id {macroDisplay.Id} does not exist"); } if (macroDisplay.Alias != macro.Alias) @@ -199,7 +198,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (macroByAlias != null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Macro with this alias already exists"); + return ValidationProblem("Macro with this alias already exists"); } } @@ -227,7 +226,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { const string errorMessage = "Error creating macro"; _logger.LogError(exception, errorMessage); - return ValidationErrorResult.CreateNotificationValidationErrorResult(errorMessage); + return ValidationProblem(errorMessage); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index b98b2e9cd7..a2f1fe36c3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -452,9 +452,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var moveResult = _mediaService.MoveToRecycleBin(foundMedia, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (moveResult == false) { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } } else @@ -462,9 +460,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var deleteResult = _mediaService.Delete(foundMedia, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (deleteResult == false) { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } } @@ -500,11 +496,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (sourceParentID == destinationParentID) { - return new ValidationErrorResult(new SimpleNotificationModel(new BackOfficeNotification("",_localizedTextService.Localize("media/moveToSameFolderFailed"),NotificationStyle.Error))); + return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification("",_localizedTextService.Localize("media/moveToSameFolderFailed"),NotificationStyle.Error))); } if (moveResult == false) { - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } else { @@ -563,9 +559,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the model state to the outgoing object and throw validation response - var forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(forDisplay); + MediaItemDisplay forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); + return ValidationProblem(forDisplay, ModelState); } } @@ -578,8 +573,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 if (!ModelState.IsValid) { - display.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(display, StatusCodes.Status403Forbidden); + return ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden); } //put the correct msgs in @@ -602,7 +596,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // is no Id to redirect to! if (saveStatus.Result.Result == OperationResultType.FailedCancelledByEvent && IsCreatingAction(contentItem.Action)) { - return new ValidationErrorResult(display); + return ValidationProblem(display); } } @@ -622,7 +616,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { _mediaService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return new UmbracoNotificationSuccessResponse(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Ok(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); } /// @@ -661,7 +655,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (_mediaService.Sort(sortedMedia) == false) { _logger.LogWarning("Media sorting failed, this was probably caused by an event being cancelled"); - return new ValidationErrorResult("Media sorting failed, this was probably caused by an event being cancelled"); + return ValidationProblem("Media sorting failed, this was probably caused by an event being cancelled"); } return Ok(); } @@ -919,7 +913,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } else { - return new ValidationErrorResult("The request was not formatted correctly, the parentId is not an integer, Guid or UDI"); + return ValidationProblem("The request was not formatted correctly, the parentId is not an integer, Guid or UDI"); } } @@ -931,7 +925,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var authorizationResult = await _authorizationService.AuthorizeAsync(User, new MediaPermissionsResource(_mediaService.GetById(intParentId)), requirement); if (!authorizationResult.Succeeded) { - return new ValidationErrorResult( + return ValidationProblem( new SimpleNotificationModel(new BackOfficeNotification( _localizedTextService.Localize("speechBubbles/operationFailedHeader"), _localizedTextService.Localize("speechBubbles/invalidUserPermissionsText"), @@ -970,7 +964,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var notificationModel = new SimpleNotificationModel(); notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(notificationModel); } } else @@ -988,7 +982,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var notificationModel = new SimpleNotificationModel(); notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByContentType"), ""); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(notificationModel); } // Check on paths @@ -996,7 +990,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var notificationModel = new SimpleNotificationModel(); notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(notificationModel); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs index 5f61d1b1c1..3ca33c3643 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs @@ -266,7 +266,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)] @@ -277,7 +277,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)] diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 6045cec8f9..7f4270c3d6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -26,11 +26,8 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.ModelBinders; -using Umbraco.Cms.Web.BackOffice.Security; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Filters; @@ -260,8 +257,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { MemberDisplay forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(forDisplay); + return ValidationProblem(forDisplay, ModelState); } // Create a scope here which will wrap all child data operations in a single transaction. @@ -300,8 +296,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // lastly, if it is not valid, add the model state to the outgoing object and throw a 403 if (!ModelState.IsValid) { - display.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(display, StatusCodes.Status403Forbidden); + return ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden); } // put the correct messages in @@ -373,7 +368,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (created.Succeeded == false) { - return new ValidationErrorResult(created.Errors.ToErrorMessage()); + return ValidationProblem(created.Errors.ToErrorMessage()); } // now re-look up the member, which will now exist @@ -460,7 +455,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers MemberIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); if (identityMember == null) { - return new ValidationErrorResult("Member was not found"); + return ValidationProblem("Member was not found"); } // Handle unlocking with the member manager (takes care of other nuances) @@ -469,7 +464,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IdentityResult unlockResult = await _memberManager.SetLockoutEndDateAsync(identityMember, DateTimeOffset.Now.AddMinutes(-1)); if (unlockResult.Succeeded == false) { - return new ValidationErrorResult( + return ValidationProblem( $"Could not unlock for member {contentItem.Id} - error {unlockResult.Errors.ToErrorMessage()}"); } needsResync = true; @@ -478,7 +473,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { // NOTE: This should not ever happen unless someone is mucking around with the request data. // An admin cannot simply lock a user, they get locked out by password attempts, but an admin can unlock them - return new ValidationErrorResult("An admin cannot lock a member"); + return ValidationProblem("An admin cannot lock a member"); } // If we're changing the password... @@ -488,13 +483,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); if (validatePassword.Succeeded == false) { - return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage()); + return ValidationProblem(validatePassword.Errors.ToErrorMessage()); } Attempt intId = identityMember.Id.TryConvertTo(); if (intId.Success == false) { - return new ValidationErrorResult("Member ID was not valid"); + return ValidationProblem("Member ID was not valid"); } var changingPasswordModel = new ChangingPasswordModel @@ -513,7 +508,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError?.ErrorMessage ?? string.Empty); } - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } needsResync = true; @@ -622,7 +617,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IdentityResult identityResult = await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove); if (!identityResult.Succeeded) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(identityResult.Errors.ToErrorMessage()); + return ValidationProblem(identityResult.Errors.ToErrorMessage()); } hasChanges = true; } @@ -635,7 +630,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, toAdd); if (!identityResult.Succeeded) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(identityResult.Errors.ToErrorMessage()); + return ValidationProblem(identityResult.Errors.ToErrorMessage()); } hasChanges = true; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 99dcf161ab..9fd95755f5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -70,14 +70,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult PostSavePackage(PackageDefinition model) { if (ModelState.IsValid == false) - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); //save it if (!_packagingService.SaveCreatedPackage(model)) - return ValidationErrorResult.CreateNotificationValidationErrorResult( + { + return ValidationProblem( model.Id == default ? $"A package with the name {model.Name} already exists" : $"The package with id {model.Id} was not found"); + } _packagingService.ExportCreatedPackage(model); @@ -108,7 +110,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var fullPath = _hostingEnvironment.MapPathWebRoot(package.PackagePath); if (!System.IO.File.Exists(fullPath)) - return ValidationErrorResult.CreateNotificationValidationErrorResult("No file found for path " + package.PackagePath); + return ValidationProblem("No file found for path " + package.PackagePath); var fileName = Path.GetFileName(package.PackagePath); @@ -116,7 +118,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var cd = new System.Net.Mime.ContentDisposition { - FileName = WebUtility.UrlEncode(fileName), + FileName = WebUtility.UrlEncode(fileName), Inline = false // false = prompt the user for downloading; true = browser to try to show the file inline }; Response.Headers.Add("Content-Disposition", cd.ToString()); @@ -132,7 +134,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult GetInstalledPackageById(int id) { var pack = _packagingService.GetInstalledPackageById(id); - if (pack == null) return NotFound(); + if (pack == null) + return NotFound(); return pack; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index 1c874732c4..da2205f59c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -187,7 +187,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (installType == PackageInstallType.AlreadyInstalled) { //this package is already installed - return ValidationErrorResult.CreateNotificationValidationErrorResult( + return ValidationProblem( _localizedTextService.Localize("packager/packageAlreadyInstalled")); } @@ -241,7 +241,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (installType == PackageInstallType.AlreadyInstalled) { - return ValidationErrorResult.CreateNotificationValidationErrorResult( + return ValidationProblem( _localizedTextService.Localize("packager/packageAlreadyInstalled")); } @@ -267,7 +267,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var packageMinVersion = packageInfo.UmbracoVersion; if (_umbracoVersion.Version < packageMinVersion) - return ValidationErrorResult.CreateNotificationValidationErrorResult( + return ValidationProblem( _localizedTextService.Localize("packager/targetVersionMismatch", new[] {packageMinVersion.ToString()})); } @@ -286,7 +286,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //save to the installedPackages.config, this will create a new entry with a new Id if (!_packagingService.SaveInstalledPackage(packageDefinition)) - return ValidationErrorResult.CreateNotificationValidationErrorResult("Could not save the package"); + return ValidationProblem("Could not save the package"); model.Id = packageDefinition.Id; break; diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index 187d59b446..9d95dee395 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -154,7 +154,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { _logger.LogError(ex, "Error creating relation type with {Name}", relationType.Name); - return ValidationErrorResult.CreateNotificationValidationErrorResult("Error creating relation type."); + return ValidationProblem("Error creating relation type."); } } @@ -169,7 +169,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (relationTypePersisted == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Relation type does not exist"); + return ValidationProblem("Relation type does not exist"); } _umbracoMapper.Map(relationType, relationTypePersisted); @@ -185,7 +185,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { _logger.LogError(ex, "Error saving relation type with {Id}", relationType.Id); - return ValidationErrorResult.CreateNotificationValidationErrorResult("Something went wrong when saving the relation type"); + return ValidationProblem("Something went wrong when saving the relation type"); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs index 3891550f1e..a370f48ebe 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs @@ -1,10 +1,17 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Web.BackOffice.Filters; +using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Filters; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -25,6 +32,91 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [MiddlewareFilter(typeof(UnhandledExceptionLoggerFilter))] public abstract class UmbracoAuthorizedApiController : UmbracoApiController { + /// + /// Returns a validation problem result for the and the + /// + /// + /// + /// + /// + protected virtual ActionResult ValidationProblem(IErrorModel model, ModelStateDictionary modelStateDictionary, int statusCode = StatusCodes.Status400BadRequest) + { + model.Errors = modelStateDictionary.ToErrorDictionary(); + return ValidationProblem(model, statusCode); + } + /// + /// Overridden to return Umbraco compatible errors + /// + /// + /// + [NonAction] + public override ActionResult ValidationProblem(ModelStateDictionary modelStateDictionary) + { + return new ValidationErrorResult(new SimpleValidationModel(modelStateDictionary.ToErrorDictionary())); + + //ValidationProblemDetails problemDetails = GetValidationProblemDetails(modelStateDictionary: modelStateDictionary); + //return new ValidationErrorResult(problemDetails); + } + + // creates validation problem details instance. + // borrowed from netcore: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1970 + protected ValidationProblemDetails GetValidationProblemDetails( + string detail = null, + string instance = null, + int? statusCode = null, + string title = null, + string type = null, + [ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null) + { + modelStateDictionary ??= ModelState; + + ValidationProblemDetails validationProblem; + if (ProblemDetailsFactory == null) + { + // ProblemDetailsFactory may be null in unit testing scenarios. Improvise to make this more testable. + validationProblem = new ValidationProblemDetails(modelStateDictionary) + { + Detail = detail, + Instance = instance, + Status = statusCode, + Title = title, + Type = type, + }; + } + else + { + validationProblem = ProblemDetailsFactory?.CreateValidationProblemDetails( + HttpContext, + modelStateDictionary, + statusCode: statusCode, + title: title, + type: type, + detail: detail, + instance: instance); + } + + return validationProblem; + } + + /// + /// Returns an Umbraco compatible validation problem for the given error message + /// + /// + /// + protected virtual ActionResult ValidationProblem(string errorMessage) + { + ValidationProblemDetails problemDetails = GetValidationProblemDetails(errorMessage); + return new ValidationErrorResult(problemDetails); + } + + /// + /// Returns an Umbraco compatible validation problem for the object result + /// + /// + /// + /// + protected virtual ActionResult ValidationProblem(object value, int statusCode = StatusCodes.Status400BadRequest) + => new ValidationErrorResult(value, statusCode); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs index f5cdb94d37..971d7400de 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs @@ -10,7 +10,6 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.BackOffice.ActionResults; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; @@ -22,7 +21,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] [PrefixlessBodyModelValidator] - public class UserGroupsController : UmbracoAuthorizedJsonController + public class UserGroupsController : BackOfficeNotificationsController { private readonly IUserService _userService; private readonly IContentService _contentService; @@ -202,10 +201,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _userService.DeleteUserGroup(userGroup); } if (userGroups.Length > 1) - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/deleteUserGroupSuccess", new[] {userGroups[0].Name})); + { + return Ok(_localizedTextService.Localize("speechBubbles/deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); + } + + return Ok(_localizedTextService.Localize("speechBubbles/deleteUserGroupSuccess", new[] {userGroups[0].Name})); } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index ee09b7d67b..ff2e087aa4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -51,7 +51,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] [PrefixlessBodyModelValidator] [IsCurrentUserModelFilter] - public class UsersController : UmbracoAuthorizedJsonController + public class UsersController : BackOfficeNotificationsController { private readonly MediaFileManager _mediaFileManager; private readonly ContentSettings _contentSettings; @@ -128,7 +128,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var urls = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); if (urls == null) - return new ValidationErrorResult("Could not access Gravatar endpoint"); + return ValidationProblem("Could not access Gravatar endpoint"); return urls; } @@ -345,7 +345,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } if (_securitySettings.UsernameIsEmail) @@ -362,7 +362,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } //Perform authorization here to see if the current user can actually save this user with the info being requested @@ -380,7 +380,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var created = await _userManager.CreateAsync(identityUser); if (created.Succeeded == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(created.Errors.ToErrorMessage()); + return ValidationProblem(created.Errors.ToErrorMessage()); } string resetPassword; @@ -389,7 +389,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var result = await _userManager.AddPasswordAsync(identityUser, password); if (result.Succeeded == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(created.Errors.ToErrorMessage()); + return ValidationProblem(created.Errors.ToErrorMessage()); } resetPassword = password; @@ -446,19 +446,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } if (!_emailSender.CanSendRequiredEmail()) { - return new ValidationErrorResult("No Email server is configured"); + return ValidationProblem("No Email server is configured"); } //Perform authorization here to see if the current user can actually save this user with the info being requested var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { - return new ValidationErrorResult(canSaveUser.Result, StatusCodes.Status401Unauthorized); + return ValidationProblem(canSaveUser.Result, StatusCodes.Status401Unauthorized); } if (user == null) @@ -471,7 +471,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var created = await _userManager.CreateAsync(identityUser); if (created.Succeeded == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(created.Errors.ToErrorMessage()); + return ValidationProblem(created.Errors.ToErrorMessage()); } //now re-look the user back up @@ -513,7 +513,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError( _securitySettings.UsernameIsEmail ? "Email" : "Username", "A user with the username already exists"); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } return new ActionResult(user); @@ -568,7 +568,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } var intId = userSave.Id.TryConvertTo(); @@ -631,7 +631,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } if (hasErrors) - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); //merge the save data onto the user var user = _umbracoMapper.Map(userSave, found); @@ -664,7 +664,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } Attempt intId = changingPasswordModel.Id.TryConvertTo(); @@ -684,12 +684,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // if it's the current user, the current user cannot reset their own password without providing their old password if (currentUser.Username == found.Username && string.IsNullOrEmpty(changingPasswordModel.OldPassword)) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Password reset is not allowed without providing old password"); + return ValidationProblem("Password reset is not allowed without providing old password"); } if (!currentUser.IsAdmin() && found.IsAdmin()) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("The current user cannot change the password for the specified user"); + return ValidationProblem("The current user cannot change the password for the specified user"); } Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _userManager); @@ -706,7 +706,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); } - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } @@ -720,7 +720,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var tryGetCurrentUserId = _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId(); if (tryGetCurrentUserId && userIds.Contains(tryGetCurrentUserId.Result)) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("The current user cannot disable itself"); + return ValidationProblem("The current user cannot disable itself"); } var users = _userService.GetUsersById(userIds).ToArray(); @@ -733,12 +733,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (users.Length > 1) { - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/disableUsersSuccess", new[] {userIds.Length.ToString()})); + return Ok(_localizedTextService.Localize("speechBubbles/disableUsersSuccess", new[] {userIds.Length.ToString()})); } - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/disableUserSuccess", new[] { users[0].Name })); + return Ok(_localizedTextService.Localize("speechBubbles/disableUserSuccess", new[] { users[0].Name })); } /// @@ -757,11 +755,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (users.Length > 1) { - return new UmbracoNotificationSuccessResponse( + return Ok( _localizedTextService.Localize("speechBubbles/enableUsersSuccess", new[] { userIds.Length.ToString() })); } - return new UmbracoNotificationSuccessResponse( + return Ok( _localizedTextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name })); } @@ -787,18 +785,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var unlockResult = await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now.AddMinutes(-1)); if (unlockResult.Succeeded == false) { - return new ValidationErrorResult( + return ValidationProblem( $"Could not unlock for user {u} - error {unlockResult.Errors.ToErrorMessage()}"); } if (userIds.Length == 1) { - return new UmbracoNotificationSuccessResponse( + return Ok( _localizedTextService.Localize("speechBubbles/unlockUserSuccess", new[] {user.Name})); } } - return new UmbracoNotificationSuccessResponse( + return Ok( _localizedTextService.Localize("speechBubbles/unlockUsersSuccess", new[] {(userIds.Length - notFound.Count).ToString()})); } @@ -816,7 +814,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } _userService.Save(users); - return new UmbracoNotificationSuccessResponse( + return Ok( _localizedTextService.Localize("speechBubbles/setUserGroupOnUsersSuccess")); } @@ -847,7 +845,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var userName = user.Name; _userService.Delete(user, true); - return new UmbracoNotificationSuccessResponse( + return Ok( _localizedTextService.Localize("speechBubbles/deleteUserSuccess", new[] { userName })); } diff --git a/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs index a48f46f605..a08cd20071 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs @@ -6,18 +6,11 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Umbraco.Cms.Web.BackOffice.PropertyEditors.Validation; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.BackOffice.Extensions +namespace Umbraco.Extensions { public static class ModelStateExtensions { - /// - /// Checks if there are any model errors on any fields containing the prefix - /// - /// - /// - /// - public static bool IsValid(this ModelStateDictionary state, string prefix) => - state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any()); + /// /// Adds the to the model state with the appropriate keys for property errors @@ -171,42 +164,5 @@ namespace Umbraco.Cms.Web.BackOffice.Extensions } } - - public static IDictionary ToErrorDictionary(this ModelStateDictionary modelState) - { - var modelStateError = new Dictionary(); - foreach (KeyValuePair keyModelStatePair in modelState) - { - var key = keyModelStatePair.Key; - ModelErrorCollection errors = keyModelStatePair.Value.Errors; - if (errors != null && errors.Count > 0) - { - modelStateError.Add(key, errors.Select(error => error.ErrorMessage)); - } - } - return modelStateError; - } - - /// - /// Serializes the ModelState to JSON for JavaScript to interrogate the errors - /// - /// - /// - public static JsonResult ToJsonErrors(this ModelStateDictionary state) => - new JsonResult(new - { - success = state.IsValid.ToString().ToLower(), - failureType = "ValidationError", - validationErrors = from e in state - where e.Value.Errors.Count > 0 - select new - { - name = e.Key, - errors = e.Value.Errors.Select(x => x.ErrorMessage) - .Concat( - e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception.Message)) - } - }); - } } diff --git a/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs b/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs index e3279407fa..92f5d7aed7 100644 --- a/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs +++ b/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs @@ -1,8 +1,9 @@ -using System.Net; +using System.Net; using Microsoft.AspNetCore.Mvc; namespace Umbraco.Cms.Web.Common.ActionsResults { + // TODO: What is the purpose of this? Doesn't seem to add any benefit public class UmbracoProblemResult : ObjectResult { public UmbracoProblemResult(string message, HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError) : base(new {Message = message}) diff --git a/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs b/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs index 8fe0ef9326..378be18440 100644 --- a/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs +++ b/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs @@ -1,10 +1,19 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.ActionsResults { + // TODO: This should probably follow the same conventions as in aspnet core and use ProblemDetails + // and ProblemDetails factory. See https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1977 + // ProblemDetails is explicitly checked for in the application model. + // In our base class UmbracoAuthorizedApiController the logic is there to create a ProblemDetails. + // However, to do this will require changing how angular deals with errors since the response will + // probably be different. Would just be better to follow the aspnet patterns. + /// /// Custom result to return a validation error message with required headers /// @@ -13,6 +22,11 @@ namespace Umbraco.Cms.Web.Common.ActionsResults /// public class ValidationErrorResult : ObjectResult { + /// + /// Typically this should not be used and just use the ValidationProblem method on the base controller class. + /// + /// + /// public static ValidationErrorResult CreateNotificationValidationErrorResult(string errorMessage) { var notificationModel = new SimpleNotificationModel @@ -23,6 +37,9 @@ namespace Umbraco.Cms.Web.Common.ActionsResults return new ValidationErrorResult(notificationModel); } + public ValidationErrorResult(ModelStateDictionary modelState) + : this(new SimpleValidationModel(modelState.ToErrorDictionary())) { } + public ValidationErrorResult(object value, int statusCode) : base(value) { StatusCode = statusCode; @@ -32,6 +49,7 @@ namespace Umbraco.Cms.Web.Common.ActionsResults { } + // TODO: Like here, shouldn't we use ProblemDetails? public ValidationErrorResult(string errorMessage, int statusCode) : base(new { Message = errorMessage }) { StatusCode = statusCode; diff --git a/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs b/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs new file mode 100644 index 0000000000..acc2858ece --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Umbraco.Extensions +{ + public static class ModelStateExtensions + { + /// + /// Checks if there are any model errors on any fields containing the prefix + /// + /// + /// + /// + public static bool IsValid(this ModelStateDictionary state, string prefix) => + state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any()); + + public static IDictionary ToErrorDictionary(this ModelStateDictionary modelState) + { + var modelStateError = new Dictionary(); + foreach (KeyValuePair keyModelStatePair in modelState) + { + var key = keyModelStatePair.Key; + ModelErrorCollection errors = keyModelStatePair.Value.Errors; + if (errors != null && errors.Count > 0) + { + modelStateError.Add(key, errors.Select(error => error.ErrorMessage)); + } + } + return modelStateError; + } + + /// + /// Serializes the ModelState to JSON for JavaScript to interrogate the errors + /// + /// + /// + public static JsonResult ToJsonErrors(this ModelStateDictionary state) => + new JsonResult(new + { + success = state.IsValid.ToString().ToLower(), + failureType = "ValidationError", + validationErrors = from e in state + where e.Value.Errors.Count > 0 + select new + { + name = e.Key, + errors = e.Value.Errors.Select(x => x.ErrorMessage) + .Concat( + e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception.Message)) + } + }); + } +}