diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index 0791707dea..9e9f7e8fbd 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Extensions; using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Common.Filters; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.BackOffice.Controllers @@ -25,7 +26,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// An abstract base controller used for media/content/members to try to reduce code replication. /// - //[JsonDateTimeFormatAttribute] //TODO Reintroduce + [JsonDateTimeFormat] public abstract class ContentControllerBase : BackOfficeNotificationsController { protected ICultureDictionary CultureDictionary { get; } diff --git a/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs index 85eb55b6d9..e185491f0e 100644 --- a/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.Common.Filters { public AngularJsonOnlyConfigurationAttribute() : base(typeof(AngularJsonOnlyConfigurationFilter)) { + Order = -2400; } private class AngularJsonOnlyConfigurationFilter : IResultFilter diff --git a/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs b/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs new file mode 100644 index 0000000000..dc4b0ad483 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs @@ -0,0 +1,60 @@ +using System.Buffers; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Umbraco.Web.Common.Formatters; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// Applying this attribute to any controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// + public class JsonDateTimeFormatAttribute : TypeFilterAttribute + { + public JsonDateTimeFormatAttribute() : base(typeof(JsonDateTimeFormatFilter)) + { + Order = -2000; + } + + private class JsonDateTimeFormatFilter : IResultFilter + { + private readonly string _format = "yyyy-MM-dd HH:mm:ss"; + + private readonly IOptions _mvcNewtonsoftJsonOptions; + private readonly ArrayPool _arrayPool; + private readonly IOptions _options; + + public JsonDateTimeFormatFilter(IOptions mvcNewtonsoftJsonOptions, ArrayPool arrayPool, IOptions options) + { + _mvcNewtonsoftJsonOptions = mvcNewtonsoftJsonOptions; + _arrayPool = arrayPool; + _options = options; + } + public void OnResultExecuted(ResultExecutedContext context) + { + } + + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is ObjectResult objectResult) + { + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.Converters.Add( + new IsoDateTimeConverter + { + DateTimeFormat = _format + }); + + objectResult.Formatters.Clear(); + objectResult.Formatters.Add(new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options.Value)); + } + } + } + } + + +} diff --git a/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs b/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs deleted file mode 100644 index b4a71a8840..0000000000 --- a/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Mapping; -using Umbraco.Core.Persistence; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Web.Routing; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; - -namespace Umbraco.Web.Editors -{ - /// - /// An abstract controller that automatically checks if any request is a non-GET and if the - /// resulting message is INotificationModel in which case it will append any Event Messages - /// currently in the request. - /// - //[AppendCurrentEventMessages] // Moved to netcore - [PrefixlessBodyModelValidator] - public abstract class BackOfficeNotificationsController : UmbracoAuthorizedJsonController - { - protected BackOfficeNotificationsController( - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, - IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) - { - } - } -} diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs deleted file mode 100644 index d9dd90f541..0000000000 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Web.Http; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Dictionary; -using Umbraco.Core.Logging; -using Umbraco.Core.Mapping; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.Persistence; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Web.Composing; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Routing; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; - -namespace Umbraco.Web.Editors -{ - /// - /// An abstract base controller used for media/content/members to try to reduce code replication. - /// - [JsonDateTimeFormatAttribute] - public abstract class ContentControllerBase : BackOfficeNotificationsController - { - protected ICultureDictionary CultureDictionary { get; } - - protected ContentControllerBase( - ICultureDictionary cultureDictionary, - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, - IPublishedUrlProvider publishedUrlProvider) - :base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) - { - CultureDictionary = cultureDictionary; - } - - protected HttpResponseMessage HandleContentNotFound(object id, bool throwException = true) - { - ModelState.AddModelError("id", $"content with id: {id} was not found"); - var errorResponse = Request.CreateErrorResponse( - HttpStatusCode.NotFound, - ModelState); - if (throwException) - { - throw new HttpResponseException(errorResponse); - } - return errorResponse; - } - - /// - /// Maps the dto property values to the persisted model - /// - internal void MapPropertyValuesForPersistence( - TSaved contentItem, - ContentPropertyCollectionDto dto, - Func getPropertyValue, - Action savePropertyValue, - string culture) - where TPersisted : IContentBase - where TSaved : IContentSave - { - // map the property values - foreach (var propertyDto in dto.Properties) - { - // get the property editor - if (propertyDto.PropertyEditor == null) - { - Logger.Warn("No property editor found for property {PropertyAlias}", propertyDto.Alias); - continue; - } - - // get the value editor - // nothing to save/map if it is readonly - var valueEditor = propertyDto.PropertyEditor.GetValueEditor(); - if (valueEditor.IsReadOnly) continue; - - // get the property - var property = contentItem.PersistedContent.Properties[propertyDto.Alias]; - - // prepare files, if any matching property and culture - var files = contentItem.UploadedFiles - .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment) - .ToArray(); - - foreach (var file in files) - file.FileName = file.FileName.ToSafeFileName(ShortStringHelper); - - // create the property data for the property editor - var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType.Configuration) - { - ContentKey = contentItem.PersistedContent.Key, - PropertyTypeKey = property.PropertyType.Key, - Files = files - }; - - // let the editor convert the value that was received, deal with files, etc - var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property)); - - // set the value - tags are special - var tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); - if (tagAttribute != null) - { - var tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType.Configuration); - if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter; - var tagCulture = property.PropertyType.VariesByCulture() ? culture : null; - property.SetTagsValue(value, tagConfiguration, tagCulture); - } - else - savePropertyValue(contentItem, property, value); - } - } - - protected virtual void HandleInvalidModelState(IErrorModel display) - { - //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(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - } - - /// - /// A helper method to attempt to get the instance from the request storage if it can be found there, - /// otherwise gets it from the callback specified - /// - /// - /// - /// - /// - /// This is useful for when filters have already looked up a persisted entity and we don't want to have - /// to look it up again. - /// - protected TPersisted GetObjectFromRequest(Func getFromService) - { - //checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return - // it from the callback - return Request.Properties.ContainsKey(typeof(TPersisted).ToString()) && Request.Properties[typeof(TPersisted).ToString()] != null - ? (TPersisted) Request.Properties[typeof (TPersisted).ToString()] - : getFromService(); - } - - /// - /// Returns true if the action passed in means we need to create something new - /// - /// - /// - internal static bool IsCreatingAction(ContentSaveAction action) - { - return (action.ToString().EndsWith("New")); - } - - protected void AddCancelMessage(INotificationModel display, - string header = "speechBubbles/operationCancelledHeader", - string message = "speechBubbles/operationCancelledText", - bool localizeHeader = true, - bool localizeMessage = true, - string[] headerParams = null, - string[] messageParams = null) - { - //if there's already a default event message, don't add our default one - // TODO: inject - var msgs = Current.EventMessages; - if (msgs != null && msgs.GetAll().Any(x => x.IsDefaultEventMessage)) return; - - display.AddWarningNotification( - localizeHeader ? Services.TextService.Localize(header, headerParams) : header, - localizeMessage ? Services.TextService.Localize(message, messageParams): message); - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f41460b66b..8db1e9ea76 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -237,7 +237,6 @@ - @@ -323,7 +322,6 @@ - @@ -362,7 +360,6 @@ - diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs deleted file mode 100644 index 7f05d59a18..0000000000 --- a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Linq; -using System.Net.Http.Formatting; -using System.Web.Http.Controllers; -using Newtonsoft.Json.Converters; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// Sets the json outgoing/serialized datetime format - /// - internal sealed class JsonDateTimeFormatAttributeAttribute : Attribute, IControllerConfiguration - { - private readonly string _format = "yyyy-MM-dd HH:mm:ss"; - - /// - /// Specify a custom format - /// - /// - public JsonDateTimeFormatAttributeAttribute(string format) - { - if (format == null) throw new ArgumentNullException(nameof(format)); - if (string.IsNullOrEmpty(format)) throw new ArgumentException("Value can't be empty.", nameof(format)); - - _format = format; - } - - /// - /// Will use the standard ISO format - /// - public JsonDateTimeFormatAttributeAttribute() - { - - } - - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - var jsonFormatter = controllerSettings.Formatters.OfType(); - foreach (var r in jsonFormatter) - { - r.SerializerSettings.Converters.Add( - new IsoDateTimeConverter - { - DateTimeFormat = _format - }); - } - } - - } -}