diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs
index 7f8cc95a13..dd4ced35d9 100644
--- a/src/Umbraco.Core/StringExtensions.cs
+++ b/src/Umbraco.Core/StringExtensions.cs
@@ -28,6 +28,21 @@ namespace Umbraco.Core
[UmbracoWillObsolete("Do not use this constants. See IShortStringHelper.CleanStringForSafeAliasJavaScriptCode.")]
public const string UmbracoInvalidFirstCharacters = "01234567890";
+ ///
+ /// Returns a stream from a string
+ ///
+ ///
+ ///
+ internal static Stream GenerateStreamFromString(this string s)
+ {
+ var stream = new MemoryStream();
+ var writer = new StreamWriter(stream);
+ writer.Write(s);
+ writer.Flush();
+ stream.Position = 0;
+ return stream;
+ }
+
///
/// This will append the query string to the URL
///
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index 82aa5c480d..7e6cf80bcd 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -87,6 +87,7 @@
..\packages\RhinoMocks.3.6.1\lib\net\Rhino.Mocks.dll
+
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/valpropertymsg.directive.js
index c3662690af..2fcd21d0ec 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/valpropertymsg.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/valpropertymsg.directive.js
@@ -53,7 +53,7 @@ function valPropertyMsg(serverValidationService) {
hasError = true;
//update the validation message if we don't already have one assigned.
if (showValidation && scope.errorMsg === "") {
- scope.errorMsg = serverValidationService.getError(scope.currentProperty, "");
+ scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "");
}
}
else {
@@ -71,7 +71,7 @@ function valPropertyMsg(serverValidationService) {
scope.$on("saving", function (ev, args) {
showValidation = true;
if (hasError && scope.errorMsg === "") {
- scope.errorMsg = serverValidationService.getError(scope.currentProperty, "");
+ scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "");
}
else if (!hasError) {
scope.errorMsg = "";
@@ -111,7 +111,7 @@ function valPropertyMsg(serverValidationService) {
//error collection... it is a 'one-time' usage so that when the field is invalidated
//again, the message we display is the client side message.
//NOTE: 'this' in the subscribe callback context is the validation manager object.
- this.removeError(scope.currentProperty);
+ this.removePropertyError(scope.currentProperty);
//flag that the current validator is invalid
formCtrl.$setValidity('valPropertyMsg', false);
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidation.service.js
index 322f9f1654..4584d8b3d5 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidation.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidation.service.js
@@ -4,7 +4,9 @@
* @function
*
* @description
- * used to handle server side validation and wires up the UI with the messages
+ * Used to handle server side validation and wires up the UI with the messages. There are 2 types of validation messages, one
+ * is for user defined properties (called Properties) and the other is for field properties which are attached to the native
+ * model objects (not user defined). The methods below are named according to these rules: Properties vs Fields.
*/
function serverValidationService() {
@@ -23,35 +25,60 @@ function serverValidationService() {
* a particular field, otherwise we can only pinpoint that there is an error for a content property, not the
* property's specific field. This is used with the val-server directive in which the directive specifies the
* field alias to listen for.
+ * If contentProperty is null, then this subscription is for a field property (not a user defined property)
*/
subscribe: function (contentProperty, fieldName, callback) {
- if (!contentProperty || !callback) {
+ if (!callback) {
return;
}
- //don't add it if it already exists
- var exists = _.find(callbacks, function(item) {
- return item.propertyAlias === contentProperty.alias && item.fieldName === fieldName;
- });
- if (!exists) {
- callbacks.push({ propertyAlias: contentProperty.alias, fieldName: fieldName, callback: callback });
- }
+
+ if (contentProperty === null) {
+ //don't add it if it already exists
+ var exists1 = _.find(callbacks, function (item) {
+ return item.propertyAlias === null && item.fieldName === fieldName;
+ });
+ if (!exists1) {
+ callbacks.push({ propertyAlias: contentProperty.alias, fieldName: fieldName, callback: callback });
+ }
+ }
+ else if (contentProperty !== undefined) {
+ //don't add it if it already exists
+ var exists2 = _.find(callbacks, function (item) {
+ return item.propertyAlias === contentProperty.alias && item.fieldName === fieldName;
+ });
+ if (!exists2) {
+ callbacks.push({ propertyAlias: contentProperty.alias, fieldName: fieldName, callback: callback });
+ }
+ }
},
unsubscribe: function(contentProperty, fieldName) {
- if (!contentProperty) {
- return;
+
+ if (contentProperty === null) {
+
+ //remove all callbacks for the content field
+ callbacks = _.reject(callbacks, function (item) {
+ return item.propertyAlias === null && item.fieldName === fieldName;
+ });
+
}
- callbacks = _.reject(callbacks, function (item) {
- return item.propertyAlias === contentProperty.alias &&
- (item.fieldName === fieldName ||
- ((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === "")));
- });
+ else if (contentProperty !== undefined) {
+
+ //remove all callbacks for the content property
+ callbacks = _.reject(callbacks, function (item) {
+ return item.propertyAlias === contentProperty.alias &&
+ (item.fieldName === fieldName ||
+ ((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === "")));
+ });
+ }
+
+
},
/**
* @ngdoc function
- * @name getCallbacks
+ * @name getPropertyCallbacks
* @methodOf umbraco.services.serverValidationService
* @function
*
@@ -60,7 +87,7 @@ function serverValidationService() {
* This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an
* explicit field name set.
*/
- getCallbacks: function (contentProperty, fieldName) {
+ getPropertyCallbacks: function (contentProperty, fieldName) {
var found = _.filter(callbacks, function (item) {
//returns any callback that have been registered directly against the field and for only the property
return (item.propertyAlias === contentProperty.alias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === "")));
@@ -70,20 +97,75 @@ function serverValidationService() {
/**
* @ngdoc function
- * @name addError
+ * @name getFieldCallbacks
+ * @methodOf umbraco.services.serverValidationService
+ * @function
+ *
+ * @description
+ * Gets all callbacks that has been registered using the subscribe method for the field.
+ */
+ getFieldCallbacks: function (contentProperty, fieldName) {
+ var found = _.filter(callbacks, function (item) {
+ //returns any callback that have been registered directly against the field
+ return (item.propertyAlias === null && item.fieldName === fieldName);
+ });
+ return found;
+ },
+
+ /**
+ * @ngdoc function
+ * @name addFieldError
+ * @methodOf umbraco.services.serverValidationService
+ * @function
+ *
+ * @description
+ * Adds an error message for a native content item field (not a user defined property, for Example, 'Name')
+ */
+ addFieldError: function(fieldName, errorMsg) {
+ if (!fieldName) {
+ return;
+ }
+
+ //only add the item if it doesn't exist
+ if (!this.hasFieldError(fieldName)) {
+ this.items.push({
+ propertyAlias: null,
+ fieldName: fieldName,
+ errorMsg: errorMsg
+ });
+ }
+
+ //find all errors for this item
+ var errorsForCallback = _.filter(this.items, function (item) {
+ return (item.propertyAlias === null && item.fieldName === fieldName);
+ });
+ //we should now call all of the call backs registered for this error
+ var cbs = this.getFieldCallbacks(fieldName);
+ //call each callback for this error
+ for (var cb in cbs) {
+ cbs[cb].callback.apply(this, [
+ false, //pass in a value indicating it is invalid
+ errorsForCallback, //pass in the errors for this item
+ this.items]); //pass in all errors in total
+ }
+ },
+
+ /**
+ * @ngdoc function
+ * @name addPropertyError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Adds an error message for the content property
*/
- addError: function (contentProperty, fieldName, errorMsg) {
+ addPropertyError: function (contentProperty, fieldName, errorMsg) {
if (!contentProperty) {
return;
}
//only add the item if it doesn't exist
- if (!this.hasError(contentProperty, fieldName)) {
+ if (!this.hasPropertyError(contentProperty, fieldName)) {
this.items.push({
propertyAlias: contentProperty.alias,
fieldName: fieldName,
@@ -96,7 +178,7 @@ function serverValidationService() {
return (item.propertyAlias === contentProperty.alias && item.fieldName === fieldName);
});
//we should now call all of the call backs registered for this error
- var cbs = this.getCallbacks(contentProperty, fieldName);
+ var cbs = this.getPropertyCallbacks(contentProperty, fieldName);
//call each callback for this error
for (var cb in cbs) {
cbs[cb].callback.apply(this, [
@@ -108,14 +190,14 @@ function serverValidationService() {
/**
* @ngdoc function
- * @name removeError
+ * @name removePropertyError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Removes an error message for the content property
*/
- removeError: function (contentProperty, fieldName) {
+ removePropertyError: function (contentProperty, fieldName) {
if (!contentProperty) {
return;
@@ -147,14 +229,14 @@ function serverValidationService() {
/**
* @ngdoc function
- * @name getError
+ * @name getPropertyError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Gets the error message for the content property
*/
- getError: function (contentProperty, fieldName) {
+ getPropertyError: function (contentProperty, fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === contentProperty.alias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
@@ -165,14 +247,32 @@ function serverValidationService() {
/**
* @ngdoc function
- * @name hasError
+ * @name getFieldError
+ * @methodOf umbraco.services.serverValidationService
+ * @function
+ *
+ * @description
+ * Gets the error message for a content field
+ */
+ getFieldError: function (fieldName) {
+ var err = _.find(this.items, function (item) {
+ //return true if the property alias matches and if an empty field name is specified or the field name matches
+ return (item.propertyAlias === null && item.fieldName === fieldName);
+ });
+ //return generic property error message if the error doesn't exist
+ return err ? err : "Field has errors";
+ },
+
+ /**
+ * @ngdoc function
+ * @name hasPropertyError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Checks if the content property + field name combo has an error
*/
- hasError: function (contentProperty, fieldName) {
+ hasPropertyError: function (contentProperty, fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === contentProperty.alias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
@@ -180,6 +280,23 @@ function serverValidationService() {
return err ? true : false;
},
+ /**
+ * @ngdoc function
+ * @name hasFieldError
+ * @methodOf umbraco.services.serverValidationService
+ * @function
+ *
+ * @description
+ * Checks if a content field has an error
+ */
+ hasFieldError: function (fieldName) {
+ var err = _.find(this.items, function (item) {
+ //return true if the property alias matches and if an empty field name is specified or the field name matches
+ return (item.propertyAlias === null && item.fieldName === fieldName);
+ });
+ return err ? true : false;
+ },
+
/** The array of error messages */
items: []
};
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/contentedit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/contentedit.controller.js
index eb9dccf2d2..46c9ce6b17 100644
--- a/src/Umbraco.Web.UI.Client/src/views/content/contentedit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/content/contentedit.controller.js
@@ -8,6 +8,61 @@
*/
function ContentEditController($scope, $routeParams, contentResource, notificationsService, angularHelper, serverValidationService) {
+
+ /**
+ * @ngdoc function
+ * @name handleValidationErrors
+ * @methodOf ContentEditController
+ * @function
+ *
+ * @description
+ * A function to handle the validation (ModelState) errors collection which will happen on a 403 error indicating validation errors
+ */
+ function handleValidationErrors(modelState) {
+ //get a list of properties since they are contained in tabs
+ var allProps = [];
+ for (var i = 0; i < $scope.content.tabs.length; i++) {
+ for (var p = 0; p < $scope.content.tabs[i].properties.length; p++) {
+ allProps.push($scope.content.tabs[i].properties[p]);
+ }
+ }
+
+ for (var e in modelState) {
+ //the alias in model state can be in dot notation which indicates
+ // * the first part is the content property alias
+ // * the second part is the field to which the valiation msg is associated with
+ //There will always be at least 2 parts since all model errors for properties are prefixed with "Properties"
+ var parts = e.split(".");
+ if (parts.length > 1) {
+ var propertyAlias = parts[1];
+
+ //find the content property for the current error
+ var contentProperty = _.find(allProps, function(item) {
+ return (item.alias === propertyAlias);
+ });
+
+ if (contentProperty) {
+ //if it contains 2 '.' then we will wire it up to a property's field
+ if (parts.length > 2) {
+ //add an error with a reference to the field for which the validation belongs too
+ serverValidationService.addPropertyError(contentProperty, parts[2], modelState[e][0]);
+ }
+ else {
+ //add a generic error for the property, no reference to a specific field
+ serverValidationService.addPropertyError(contentProperty, "", modelState[e][0]);
+ }
+ }
+ }
+ else {
+ //the parts are only 1, this means its not a property but a native content property
+ serverValidationService.addFieldError(parts[0], modelState[e][0]);
+ }
+
+ //add to notifications
+ notificationsService.error("Validation", modelState[e][0]);
+ }
+ }
+
/**
* @ngdoc function
* @name handleSaveError
@@ -17,55 +72,19 @@ function ContentEditController($scope, $routeParams, contentResource, notificati
* @description
* A function to handle what happens when we have validation issues from the server side
*/
-
function handleSaveError(err) {
//When the status is a 403 status, we have validation errors.
//Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
//Or, some strange server error
if (err.status == 403) {
//now we need to look through all the validation errors
- if (err.data && err.data.ModelState) {
-
- //get a list of properties since they are contained in tabs
- var allProps = [];
- for (var i = 0; i < $scope.content.tabs.length; i++) {
- for (var p = 0; p < $scope.content.tabs[i].properties.length; p++) {
- allProps.push($scope.content.tabs[i].properties[p]);
- }
- }
-
- for (var e in err.data.ModelState) {
- //the alias in model state can be in dot notation which indicates
- // * the first part is the content property alias
- // * the second part is the field to which the valiation msg is associated with
- var parts = e.split(".");
- var propertyAlias = parts[0];
-
- //find the content property for the current error
- var contentProperty = _.find(allProps, function (item) {
- return (item.alias === propertyAlias);
- });
- if (contentProperty) {
- //if it contains a '.' then we will wire it up to a property's field
- if (parts.length > 1) {
- //add an error with a reference to the field for which the validation belongs too
- serverValidationService.addError(contentProperty, parts[1], err.data.ModelState[e][0]);
- }
- else {
- //add a generic error for the property, no reference to a specific field
- serverValidationService.addError(contentProperty, "", err.data.ModelState[e][0]);
- }
-
- //add to notifications
- notificationsService.error("Validation", err.data.ModelState[e][0]);
- }
- }
-
+ if (err.data && (err.data.ModelState)) {
+
+ handleValidationErrors(err.data.ModelState);
}
}
else {
//TODO: Implement an overlay showing the full YSOD like we had in v5
- //alert("failed!");
notificationsService.error("Validation failed", err);
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-content-name.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-content-name.html
index 6602e76e18..8b412eeb8e 100644
--- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-content-name.html
+++ b/src/Umbraco.Web.UI.Client/src/views/directives/umb-content-name.html
@@ -1,6 +1,6 @@
-
+
Required
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs
index bbdfbd739f..f92e284c0e 100644
--- a/src/Umbraco.Web/Editors/ContentController.cs
+++ b/src/Umbraco.Web/Editors/ContentController.cs
@@ -4,6 +4,7 @@ using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.ModelBinding;
+using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
@@ -23,7 +24,6 @@ namespace Umbraco.Web.Editors
/// The API controller used for editing content
///
[PluginController("UmbracoApi")]
- [ValidationFilter]
public class ContentController : UmbracoAuthorizedJsonController
{
private readonly ContentModelMapper _contentModelMapper;
@@ -95,7 +95,6 @@ namespace Umbraco.Web.Editors
/// Saves content
///
///
- [ContentItemValidationFilter(typeof(ContentItemValidationHelper))]
[FileUploadCleanupFilter]
public ContentItemDisplay PostSave(
[ModelBinder(typeof(ContentItemBinder))]
@@ -107,9 +106,43 @@ namespace Umbraco.Web.Editors
// * any file attachments have been saved to their temporary location for us to use
// * we have a reference to the DTO object and the persisted object
+ //We need to manually check the validation results here because:
+ // * We still need to save the entity even if there are validation value errors
+ // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null)
+ // then we cannot continue saving, we can only display errors
+ // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display
+ // a message indicating this
+ // TODO: WE need to implement the above points!
+
+ if (!ModelState.IsValid)
+ {
+ if (ModelState["Name"] != null && ModelState["Name"].Errors.Any()
+ && (contentItem.Action == ContentSaveAction.SaveNew || contentItem.Action == ContentSaveAction.PublishNew))
+ {
+ //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
+ throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.Forbidden, ModelState));
+ }
+
+ //if the model state is not valid we cannot publish so change it to save
+ switch (contentItem.Action)
+ {
+ case ContentSaveAction.Publish:
+ contentItem.Action = ContentSaveAction.Save;
+ break;
+ case ContentSaveAction.PublishNew:
+ contentItem.Action = ContentSaveAction.SaveNew;
+ break;
+ }
+ }
+
//Now, we just need to save the data
- contentItem.PersistedContent.Name = contentItem.Name;
+ //Don't update the name if it is empty
+ if (!contentItem.Name.IsNullOrWhiteSpace())
+ {
+ contentItem.PersistedContent.Name = contentItem.Name;
+ }
+
//TODO: We'll need to save the new template, publishat, etc... values here
//Save the property values
@@ -152,7 +185,15 @@ namespace Umbraco.Web.Editors
//return the updated model
- return _contentModelMapper.ToContentItemDisplay(contentItem.PersistedContent);
+ var display = _contentModelMapper.ToContentItemDisplay(contentItem.PersistedContent);
+ //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
+ if (!ModelState.IsValid)
+ {
+ display.Errors = ModelState.ToErrorDictionary();
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, display));
+ }
+
+ return display;
}
}
diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs
index f9a183dd93..5e9dc789e7 100644
--- a/src/Umbraco.Web/Editors/MediaController.cs
+++ b/src/Umbraco.Web/Editors/MediaController.cs
@@ -111,8 +111,7 @@ namespace Umbraco.Web.Editors
///
/// Saves content
///
- ///
- [ContentItemValidationFilter(typeof(ContentItemValidationHelper))]
+ ///
[FileUploadCleanupFilter]
public MediaItemDisplay PostSave(
[ModelBinder(typeof(MediaItemBinder))]
diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs
index d2980c5aab..964b7712b4 100644
--- a/src/Umbraco.Web/ModelStateExtensions.cs
+++ b/src/Umbraco.Web/ModelStateExtensions.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
@@ -46,6 +48,21 @@ namespace Umbraco.Web
// state.AddModelError("DataValidation", errorMessage);
//}
+ public static IDictionary ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState)
+ {
+ var modelStateError = new Dictionary();
+ foreach (var keyModelStatePair in modelState)
+ {
+ var key = keyModelStatePair.Key;
+ var errors = keyModelStatePair.Value.Errors;
+ if (errors != null && errors.Count > 0)
+ {
+ modelStateError.Add(key, errors.Select(error => error.ErrorMessage));
+ }
+ }
+ return modelStateError;
+ }
+
///
/// Serializes the ModelState to JSON for JavaScript to interrogate the errors
///
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
index be12bab4fb..590b1a1cbd 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Umbraco.Core.Models;
+using Umbraco.Web.WebApi;
namespace Umbraco.Web.Models.ContentEditing
{
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
index 0fc93d9502..a5856a1763 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
@@ -1,6 +1,9 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
+using System.Web.Http;
+using System.Web.Http.ModelBinding;
using Umbraco.Core.Models;
namespace Umbraco.Web.Models.ContentEditing
@@ -8,11 +11,23 @@ namespace Umbraco.Web.Models.ContentEditing
///
/// A model representing a content item to be displayed in the back office
///
+ [DataContract(Name = "content", Namespace = "")]
public class ContentItemDisplay : TabbedContentItem
{
[DataMember(Name = "publishDate")]
public DateTime? PublishDate { get; set; }
+ ///
+ /// This is used for validation of a content item.
+ ///
+ ///
+ /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will
+ /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the
+ /// updated model.
+ ///
+ [DataMember(Name = "ModelState")]
+ public IDictionary Errors { get; set; }
+
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs
index 6d160cbeb8..b462c75280 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs
@@ -6,14 +6,10 @@ using Umbraco.Core.Models;
namespace Umbraco.Web.Models.ContentEditing
{
- public interface IHaveUploadedFiles
- {
- List UploadedFiles { get; }
- }
-
///
/// A model representing a content item to be saved
///
+ [DataContract(Name = "content", Namespace = "")]
public class ContentItemSave : ContentItemBasic, IHaveUploadedFiles
where TPersisted : IContentBase
{
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs
index 9123432993..ac01601e27 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs
@@ -7,6 +7,7 @@ namespace Umbraco.Web.Models.ContentEditing
///
/// Represents a content property that is displayed in the UI
///
+ [DataContract(Name = "property", Namespace = "")]
public class ContentPropertyDisplay : ContentPropertyBasic
{
[DataMember(Name = "label", IsRequired = true)]
diff --git a/src/Umbraco.Web/Models/ContentEditing/IHaveUploadedFiles.cs b/src/Umbraco.Web/Models/ContentEditing/IHaveUploadedFiles.cs
new file mode 100644
index 0000000000..9c96176063
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/IHaveUploadedFiles.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ public interface IHaveUploadedFiles
+ {
+ List UploadedFiles { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs
index 5900aa756d..956194034b 100644
--- a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs
@@ -7,6 +7,7 @@ namespace Umbraco.Web.Models.ContentEditing
///
/// A model representing a content item to be displayed in the back office
///
+ [DataContract(Name = "content", Namespace = "")]
public class MediaItemDisplay : TabbedContentItem
{
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 1116a4b9b1..43d3725b83 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -306,6 +306,7 @@
+
@@ -466,6 +467,7 @@
+
diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs
index 8ea7e3df66..9c0c589b79 100644
--- a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs
+++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs
@@ -1,16 +1,29 @@
-using System.IO;
+using System;
+using System.ComponentModel;
+using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
-using System.Web.Http.ModelBinding;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Validation;
+using System.Web.ModelBinding;
using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Web.WebApi.Filters;
+using IModelBinder = System.Web.Http.ModelBinding.IModelBinder;
+using ModelBindingContext = System.Web.Http.ModelBinding.ModelBindingContext;
+using ModelMetadata = System.Web.Http.Metadata.ModelMetadata;
+using ModelMetadataProvider = System.Web.Http.Metadata.ModelMetadataProvider;
+using MutableObjectModelBinder = System.Web.Http.ModelBinding.Binders.MutableObjectModelBinder;
using Task = System.Threading.Tasks.Task;
namespace Umbraco.Web.WebApi.Binders
@@ -45,13 +58,18 @@ namespace Umbraco.Web.WebApi.Binders
Directory.CreateDirectory(root);
var provider = new MultipartFormDataStreamProvider(root);
- var task = Task.Run(() => GetModel(actionContext.Request, provider))
+ var task = Task.Run(() => GetModel(actionContext, bindingContext, provider))
.ContinueWith(x =>
{
if (x.IsFaulted && x.Exception != null)
{
throw x.Exception;
}
+
+ //now that everything is binded, validate the properties
+ var contentItemValidator = new ContentItemValidationHelper(ApplicationContext);
+ contentItemValidator.ValidateItem(actionContext, x.Result);
+
bindingContext.Model = x.Result;
});
@@ -63,11 +81,14 @@ namespace Umbraco.Web.WebApi.Binders
///
/// Builds the model from the request contents
///
- ///
+ ///
+ ///
///
///
- private async Task> GetModel(HttpRequestMessage request, MultipartFormDataStreamProvider provider)
+ private async Task> GetModel(HttpActionContext actionContext, ModelBindingContext bindingContext, MultipartFormDataStreamProvider provider)
{
+ var request = actionContext.Request;
+
//IMPORTANT!!! We need to ensure the umbraco context here because this is running in an async thread
UmbracoContext.EnsureContext(request.Properties["MS_HttpContext"] as HttpContextBase, ApplicationContext.Current);
@@ -87,8 +108,14 @@ namespace Umbraco.Web.WebApi.Binders
//get the string json from the request
var contentItem = result.FormData["contentItem"];
- //transform the json into an object
+ //deserialize into our model
var model = JsonConvert.DeserializeObject>(contentItem);
+
+ //get the default body validator and validate the object
+ var bodyValidator = actionContext.ControllerContext.Configuration.Services.GetBodyModelValidator();
+ var metadataProvider = actionContext.ControllerContext.Configuration.Services.GetModelMetadataProvider();
+ //all validation errors will not contain a prefix
+ bodyValidator.Validate(model, typeof (ContentItemSave), metadataProvider, actionContext, "");
//get the files
foreach (var file in result.FileData)
@@ -150,7 +177,7 @@ namespace Umbraco.Web.WebApi.Binders
///
///
private static void MapPropertyValuesFromSaved(ContentItemSave saveModel, ContentItemDto dto)
- {
+ {
foreach (var p in saveModel.Properties)
{
dto.Properties.Single(x => x.Alias == p.Alias).Value = p.Value;
diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationFilterAttribute.cs
index 58a8c7a275..4aaf621e05 100644
--- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationFilterAttribute.cs
+++ b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationFilterAttribute.cs
@@ -1,208 +1,49 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.UI;
-using Umbraco.Core;
-using Umbraco.Core.Logging;
-using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
-using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Models.Mapping;
namespace Umbraco.Web.WebApi.Filters
{
- internal class ContentItemValidationHelper
- where TPersisted : IContentBase
- {
- private readonly ApplicationContext _applicationContext;
+ /////
+ ///// Validates the content item
+ /////
+ /////
+ ///// There's various validation happening here both value validation and structure validation
+ ///// to ensure that malicious folks are not trying to post invalid values or to invalid properties.
+ /////
+ //internal class ContentItemValidationFilterAttribute : ActionFilterAttribute
+ //{
+ // private readonly Type _helperType;
+ // private readonly dynamic _helper;
- public ContentItemValidationHelper(ApplicationContext applicationContext)
- {
- _applicationContext = applicationContext;
- }
+ // public ContentItemValidationFilterAttribute(Type helperType)
+ // {
+ // _helperType = helperType;
+ // _helper = Activator.CreateInstance(helperType);
+ // }
- public ContentItemValidationHelper()
- : this(ApplicationContext.Current)
- {
-
- }
+ // ///
+ // /// Returns true so that other filters can execute along with this one
+ // ///
+ // public override bool AllowMultiple
+ // {
+ // get { return true; }
+ // }
- public void ValidateItem(HttpActionContext actionContext)
- {
- var contentItem = actionContext.ActionArguments["contentItem"] as ContentItemSave;
- if (contentItem == null)
- {
- actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "No " + typeof(ContentItemSave) + " found in request");
- return;
- }
-
- //now do each validation step
- if (ValidateExistingContent(contentItem, actionContext) == false) return;
- if (ValidateProperties(contentItem, actionContext) == false) return;
- if (ValidateData(contentItem, actionContext) == false) return;
- }
-
- ///
- /// Ensure the content exists
- ///
- ///
- ///
- ///
- private bool ValidateExistingContent(ContentItemBasic postedItem, HttpActionContext actionContext)
- {
- if (postedItem.PersistedContent == null)
- {
- var message = string.Format("content with id: {0} was not found", postedItem.Id);
- actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
- return false;
- }
-
- return true;
- }
-
- ///
- /// Ensure all of the ids in the post are valid
- ///
- ///
- ///
- ///
- //private bool ValidateProperties(ContentItemSave postedItem, ContentItemDto realItem, HttpActionContext actionContext)
- private bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext)
- {
- foreach (var p in postedItem.Properties)
- {
- //ensure the property actually exists in our server side properties
- if (postedItem.ContentDto.Properties.Contains(p) == false)
- {
- //TODO: Do we return errors here ? If someone deletes a property whilst their editing then should we just
- //save the property data that remains? Or inform them they need to reload... not sure. This problem exists currently too i think.
-
- var message = string.Format("property with id: {0} was not found", p.Id);
- actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
- return false;
- }
- }
- return true;
- }
-
- private bool ValidateData(ContentItemBasic postedItem, HttpActionContext actionContext)
- {
- foreach (var p in postedItem.ContentDto.Properties)
- {
- var editor = p.PropertyEditor;
- if (editor == null)
- {
- var message = string.Format("The property editor with id: {0} was not found for property with id {1}", p.DataType.ControlId, p.Id);
- LogHelper.Warn>(message);
- //actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
- //return false;
- continue;
- }
-
- //get the posted value for this property
- var postedValue = postedItem.Properties.Single(x => x.Alias == p.Alias).Value;
-
- //get the pre-values for this property
- var preValues = _applicationContext.Services.DataTypeService.GetPreValueAsString(p.DataType.Id);
-
- //TODO: when we figure out how to 'override' certain pre-value properties we'll either need to:
- // * Combine the preValues with the overridden values stored with the document type property (but how to combine?)
- // * Or, pass in the overridden values stored with the doc type property separately
-
- foreach (var v in editor.ValueEditor.Validators)
- {
- foreach (var result in v.Validate(postedValue, preValues, editor))
- {
- //if there are no member names supplied then we assume that the validation message is for the overall property
- // not a sub field on the property editor
- if (!result.MemberNames.Any())
- {
- //add a model state error for the entire property
- actionContext.ModelState.AddModelError(p.Alias, result.ErrorMessage);
- }
- else
- {
- //there's assigned field names so we'll combine the field name with the property name
- // so that we can try to match it up to a real sub field of this editor
- foreach (var field in result.MemberNames)
- {
- actionContext.ModelState.AddModelError(string.Format("{0}.{1}", p.Alias, field), result.ErrorMessage);
- }
- }
- }
- }
-
- //Now we need to validate the property based on the PropertyType validation (i.e. regex and required)
- // NOTE: These will become legacy once we have pre-value overrides.
- if (p.IsRequired)
- {
- foreach (var result in p.PropertyEditor.ValueEditor.RequiredValidator.Validate(postedValue, "", preValues, editor))
- {
- //add a model state error for the entire property
- actionContext.ModelState.AddModelError(p.Alias, result.ErrorMessage);
- }
- }
-
- if (!p.ValidationRegExp.IsNullOrWhiteSpace())
- {
- foreach (var result in p.PropertyEditor.ValueEditor.RegexValidator.Validate(postedValue, p.ValidationRegExp, preValues, editor))
- {
- //add a model state error for the entire property
- actionContext.ModelState.AddModelError(p.Alias, result.ErrorMessage);
- }
- }
- }
-
- //create the response if there any errors
- if (!actionContext.ModelState.IsValid)
- {
- actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, actionContext.ModelState);
- }
-
- return actionContext.ModelState.IsValid;
- }
- }
-
- ///
- /// Validates the content item
- ///
- ///
- /// There's various validation happening here both value validation and structure validation
- /// to ensure that malicious folks are not trying to post invalid values or to invalid properties.
- ///
- internal class ContentItemValidationFilterAttribute : ActionFilterAttribute
- {
- private readonly Type _helperType;
- private dynamic _helper;
-
- public ContentItemValidationFilterAttribute(Type helperType)
- {
- _helperType = helperType;
- _helper = Activator.CreateInstance(helperType);
- }
-
- ///
- /// Returns true so that other filters can execute along with this one
- ///
- public override bool AllowMultiple
- {
- get { return true; }
- }
-
- ///
- /// Performs the validation
- ///
- ///
- public override void OnActionExecuting(HttpActionContext actionContext)
- {
- _helper.ValidateItem(actionContext);
- }
+ // ///
+ // /// Performs the validation
+ // ///
+ // ///
+ // public override void OnActionExecuting(HttpActionContext actionContext)
+ // {
+ // _helper.ValidateItem(actionContext);
+ // }
- }
+ //}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs
new file mode 100644
index 0000000000..ff85558cad
--- /dev/null
+++ b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs
@@ -0,0 +1,179 @@
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models;
+using Umbraco.Web.Models.ContentEditing;
+
+namespace Umbraco.Web.WebApi.Filters
+{
+ ///
+ /// A validation helper class used with ContentItemValidationFilterAttribute to be shared between content, media, etc...
+ ///
+ ///
+ ///
+ /// If any severe errors occur then the response gets set to an error and execution will not continue. Property validation
+ /// errors will just be added to the ModelState.
+ ///
+ internal class ContentItemValidationHelper
+ where TPersisted : IContentBase
+ {
+ private readonly ApplicationContext _applicationContext;
+
+ public ContentItemValidationHelper(ApplicationContext applicationContext)
+ {
+ _applicationContext = applicationContext;
+ }
+
+ public ContentItemValidationHelper()
+ : this(ApplicationContext.Current)
+ {
+
+ }
+
+ public void ValidateItem(HttpActionContext actionContext, string argumentName)
+ {
+ var contentItem = actionContext.ActionArguments[argumentName] as ContentItemSave;
+ if (contentItem == null)
+ {
+ actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "No " + typeof(ContentItemSave) + " found in request");
+ return;
+ }
+
+ ValidateItem(actionContext, contentItem);
+
+ }
+
+ public void ValidateItem(HttpActionContext actionContext, ContentItemSave contentItem)
+ {
+ //now do each validation step
+ if (ValidateExistingContent(contentItem, actionContext) == false) return;
+ if (ValidateProperties(contentItem, actionContext) == false) return;
+ if (ValidatePropertyData(contentItem, actionContext) == false) return;
+ }
+
+ ///
+ /// Ensure the content exists
+ ///
+ ///
+ ///
+ ///
+ private bool ValidateExistingContent(ContentItemBasic postedItem, HttpActionContext actionContext)
+ {
+ if (postedItem.PersistedContent == null)
+ {
+ var message = string.Format("content with id: {0} was not found", postedItem.Id);
+ actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Ensure all of the ids in the post are valid
+ ///
+ ///
+ ///
+ ///
+ private bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext)
+ {
+ foreach (var p in postedItem.Properties)
+ {
+ //ensure the property actually exists in our server side properties
+ if (postedItem.ContentDto.Properties.Contains(p) == false)
+ {
+ //TODO: Do we return errors here ? If someone deletes a property whilst their editing then should we just
+ //save the property data that remains? Or inform them they need to reload... not sure. This problem exists currently too i think.
+
+ var message = string.Format("property with id: {0} was not found", p.Id);
+ actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Validates the data for each property
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// All property data validation goes into the modelstate with a prefix of "Properties"
+ ///
+ private bool ValidatePropertyData(ContentItemBasic postedItem, HttpActionContext actionContext)
+ {
+ foreach (var p in postedItem.ContentDto.Properties)
+ {
+ var editor = p.PropertyEditor;
+ if (editor == null)
+ {
+ var message = string.Format("The property editor with id: {0} was not found for property with id {1}", p.DataType.ControlId, p.Id);
+ LogHelper.Warn>(message);
+ //actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
+ //return false;
+ continue;
+ }
+
+ //get the posted value for this property
+ var postedValue = postedItem.Properties.Single(x => x.Alias == p.Alias).Value;
+
+ //get the pre-values for this property
+ var preValues = _applicationContext.Services.DataTypeService.GetPreValueAsString(p.DataType.Id);
+
+ //TODO: when we figure out how to 'override' certain pre-value properties we'll either need to:
+ // * Combine the preValues with the overridden values stored with the document type property (but how to combine?)
+ // * Or, pass in the overridden values stored with the doc type property separately
+
+ foreach (var v in editor.ValueEditor.Validators)
+ {
+ foreach (var result in v.Validate(postedValue, preValues, editor))
+ {
+ //if there are no member names supplied then we assume that the validation message is for the overall property
+ // not a sub field on the property editor
+ if (!result.MemberNames.Any())
+ {
+ //add a model state error for the entire property
+ actionContext.ModelState.AddModelError(string.Format("{0}.{1}", "Properties", p.Alias), result.ErrorMessage);
+ }
+ else
+ {
+ //there's assigned field names so we'll combine the field name with the property name
+ // so that we can try to match it up to a real sub field of this editor
+ foreach (var field in result.MemberNames)
+ {
+ actionContext.ModelState.AddModelError(string.Format("{0}.{1}.{2}", "Properties", p.Alias, field), result.ErrorMessage);
+ }
+ }
+ }
+ }
+
+ //Now we need to validate the property based on the PropertyType validation (i.e. regex and required)
+ // NOTE: These will become legacy once we have pre-value overrides.
+ if (p.IsRequired)
+ {
+ foreach (var result in p.PropertyEditor.ValueEditor.RequiredValidator.Validate(postedValue, "", preValues, editor))
+ {
+ //add a model state error for the entire property
+ actionContext.ModelState.AddModelError(string.Format("{0}.{1}", "Properties", p.Alias), result.ErrorMessage);
+ }
+ }
+
+ if (!p.ValidationRegExp.IsNullOrWhiteSpace())
+ {
+ foreach (var result in p.PropertyEditor.ValueEditor.RegexValidator.Validate(postedValue, p.ValidationRegExp, preValues, editor))
+ {
+ //add a model state error for the entire property
+ actionContext.ModelState.AddModelError(string.Format("{0}.{1}", "Properties", p.Alias), result.ErrorMessage);
+ }
+ }
+ }
+
+ return actionContext.ModelState.IsValid;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/WebApi/ValidationFilterAttribute.cs b/src/Umbraco.Web/WebApi/ValidationFilterAttribute.cs
index 11c54f8e1d..07cabcbb16 100644
--- a/src/Umbraco.Web/WebApi/ValidationFilterAttribute.cs
+++ b/src/Umbraco.Web/WebApi/ValidationFilterAttribute.cs
@@ -1,10 +1,12 @@
-using System.Net;
+using System.ComponentModel.DataAnnotations;
+using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace Umbraco.Web.WebApi
{
+
///
/// An action filter used to do basic validation against the model and return a result
/// straight away if it fails.