Merge pull request #10534 from umbraco/v9/task/cleanup-validation-results

Streamlines response handling in controllers
This commit is contained in:
Bjarke Berg
2021-07-01 23:04:45 +02:00
committed by GitHub
27 changed files with 383 additions and 266 deletions

View File

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

View File

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

View File

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

View File

@@ -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
{
/// <summary>
/// returns a 200 OK response with a notification message
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
protected OkObjectResult Ok(string message)
{
var notificationModel = new SimpleNotificationModel
{
Message = message
};
notificationModel.AddSuccessNotification(message, string.Empty);
return new OkObjectResult(notificationModel);
}
/// <summary>
/// Overridden to ensure that the error message is an error notification message
/// </summary>
/// <param name="errorMessage"></param>
/// <returns></returns>
protected override ActionResult ValidationProblem(string errorMessage)
=> ValidationProblem(errorMessage, string.Empty);
/// <summary>
/// Creates a notofication validation problem with a header and message
/// </summary>
/// <param name="errorHeader"></param>
/// <param name="errorMessage"></param>
/// <returns></returns>
protected ActionResult ValidationProblem(string errorHeader, string errorMessage)
{
var notificationModel = new SimpleNotificationModel
{
Message = errorMessage
};
notificationModel.AddErrorNotification(errorHeader, errorMessage);
return new ValidationErrorResult(notificationModel);
}
/// <summary>
/// Overridden to ensure that all queued notifications are sent to the back office
/// </summary>
/// <returns></returns>
[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());
}
}

View File

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

View File

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

View File

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

View File

@@ -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
/// </summary>
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[PrefixlessBodyModelValidator]
public abstract class ContentTypeControllerBase<TContentType> : UmbracoAuthorizedJsonController
public abstract class ContentTypeControllerBase<TContentType> : 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<TContentTypeSave, TContentTypeDisplay>(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<TContentTypeDisplay, TContentTypeSave, TPropertyType>(ex, contentTypeSave, ct, ctId);
if (responseEx != null) return new ValidationErrorResult(responseEx);
if (responseEx != null) return ValidationProblem(responseEx);
}
var exResult = CreateCompositionValidationExceptionIfInvalid<TContentTypeSave, TPropertyType, TContentTypeDisplay>(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<TContentTypeSave, TPropertyType, TContentTypeDisplay>(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();
}

View File

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

View File

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

View File

@@ -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<int> 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<string, string> { { "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<string, string> { { "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");
}
}

View File

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

View File

@@ -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<IEnumerable<LogTemplate>>(_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;

View File

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

View File

@@ -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<MediaItemDisplay>(contentItem.PersistedContent);
forDisplay.Errors = ModelState.ToErrorDictionary();
return new ValidationErrorResult(forDisplay);
MediaItemDisplay forDisplay = _umbracoMapper.Map<MediaItemDisplay>(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"));
}
/// <summary>
@@ -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);
}
}

View File

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

View File

@@ -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<MemberDisplay>(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<int> intId = identityMember.Id.TryConvertTo<int>();
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;
}

View File

@@ -70,14 +70,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
public ActionResult<PackageDefinition> 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<PackageDefinition> GetInstalledPackageById(int id)
{
var pack = _packagingService.GetInstalledPackageById(id);
if (pack == null) return NotFound();
if (pack == null)
return NotFound();
return pack;
}

View File

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

View File

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

View File

@@ -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
{
/// <summary>
/// Returns a validation problem result for the <see cref="IErrorModel"/> and the <see cref="ModelStateDictionary"/>
/// </summary>
/// <param name="model"></param>
/// <param name="modelStateDictionary"></param>
/// <param name="statusCode"></param>
/// <returns></returns>
protected virtual ActionResult ValidationProblem(IErrorModel model, ModelStateDictionary modelStateDictionary, int statusCode = StatusCodes.Status400BadRequest)
{
model.Errors = modelStateDictionary.ToErrorDictionary();
return ValidationProblem(model, statusCode);
}
/// <summary>
/// Overridden to return Umbraco compatible errors
/// </summary>
/// <param name="modelStateDictionary"></param>
/// <returns></returns>
[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;
}
/// <summary>
/// Returns an Umbraco compatible validation problem for the given error message
/// </summary>
/// <param name="errorMessage"></param>
/// <returns></returns>
protected virtual ActionResult ValidationProblem(string errorMessage)
{
ValidationProblemDetails problemDetails = GetValidationProblemDetails(errorMessage);
return new ValidationErrorResult(problemDetails);
}
/// <summary>
/// Returns an Umbraco compatible validation problem for the object result
/// </summary>
/// <param name="value"></param>
/// <param name="statusCode"></param>
/// <returns></returns>
protected virtual ActionResult ValidationProblem(object value, int statusCode = StatusCodes.Status400BadRequest)
=> new ValidationErrorResult(value, statusCode);
}
}

View File

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

View File

@@ -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<IUser>(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<int>();
@@ -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<int> intId = changingPasswordModel.Id.TryConvertTo<int>();
@@ -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<PasswordChangedModel> 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 }));
}
/// <summary>
@@ -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 }));
}

View File

@@ -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
{
/// <summary>
/// Checks if there are any model errors on any fields containing the prefix
/// </summary>
/// <param name="state"></param>
/// <param name="prefix"></param>
/// <returns></returns>
public static bool IsValid(this ModelStateDictionary state, string prefix) =>
state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any());
/// <summary>
/// Adds the <see cref="ValidationResult"/> to the model state with the appropriate keys for property errors
@@ -171,42 +164,5 @@ namespace Umbraco.Cms.Web.BackOffice.Extensions
}
}
public static IDictionary<string, object> ToErrorDictionary(this ModelStateDictionary modelState)
{
var modelStateError = new Dictionary<string, object>();
foreach (KeyValuePair<string, ModelStateEntry> 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;
}
/// <summary>
/// Serializes the ModelState to JSON for JavaScript to interrogate the errors
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
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))
}
});
}
}

View File

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

View File

@@ -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.
/// <summary>
/// Custom result to return a validation error message with required headers
/// </summary>
@@ -13,6 +22,11 @@ namespace Umbraco.Cms.Web.Common.ActionsResults
/// </remarks>
public class ValidationErrorResult : ObjectResult
{
/// <summary>
/// Typically this should not be used and just use the ValidationProblem method on the base controller class.
/// </summary>
/// <param name="errorMessage"></param>
/// <returns></returns>
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;

View File

@@ -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
{
/// <summary>
/// Checks if there are any model errors on any fields containing the prefix
/// </summary>
/// <param name="state"></param>
/// <param name="prefix"></param>
/// <returns></returns>
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<string, object> ToErrorDictionary(this ModelStateDictionary modelState)
{
var modelStateError = new Dictionary<string, object>();
foreach (KeyValuePair<string, ModelStateEntry> 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;
}
/// <summary>
/// Serializes the ModelState to JSON for JavaScript to interrogate the errors
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
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))
}
});
}
}