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