diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js
index 9a077615df..c07ee26dec 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js
@@ -16,7 +16,11 @@ function valServerField(serverValidationManager) {
}
var fieldName = attr.valServerField;
-
+ var evalfieldName = scope.$eval(attr.valServerField);
+ if (evalfieldName) {
+ fieldName = evalfieldName;
+ }
+
//subscribe to the changed event of the view model. This is required because when we
// have a server error we actually invalidate the form which means it cannot be
// resubmitted. So once a field is changed that has a server error assigned to it
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
index f1ac6d2900..d21f297a91 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
@@ -65,7 +65,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
self.handleSuccessfulSave({
scope: args.scope,
savedContent: data,
- rebindCallback: rebindCallback(args.content, data)
+ rebindCallback: function() {
+ rebindCallback.apply(self, [args.content, data]);
+ }
});
args.scope.busy = false;
@@ -75,7 +77,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
self.handleSaveError({
redirectOnFailure: true,
err: err,
- rebindCallback: rebindCallback(args.content, err.data)
+ rebindCallback: function() {
+ rebindCallback.apply(self, [args.content, err.data]);
+ }
});
//show any notifications
if (angular.isArray(err.data.notifications)) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
index 057e0b8cff..e3261b7372 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
@@ -160,6 +160,16 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati
handleServerValidation: function(modelState) {
for (var e in modelState) {
+ //This is where things get interesting....
+ // We need to support validation for all editor types such as both the content and content type editors.
+ // The Content editor ModelState is quite specific with the way that Properties are validated especially considering
+ // that each property is a User Developer property editor.
+ // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations
+ // system.
+ // So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties,
+ // which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect
+ // this, then we know it's a Property.
+
//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
@@ -167,7 +177,11 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati
//If it is not prefixed with "Properties" that means the error is for a field of the object directly.
var parts = e.split(".");
- if (parts.length > 1) {
+
+ //Check if this is for content properties - specific to content/media/member editors because those are special
+ // user defined properties with custom controls.
+ if (parts.length > 1 && parts[0] === "_Properties") {
+
var propertyAlias = parts[1];
//if it contains 2 '.' then we will wire it up to a property's field
@@ -182,8 +196,10 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati
}
else {
- //the parts are only 1, this means its not a property but a native content property
- serverValidationManager.addFieldError(parts[0], modelState[e][0]);
+
+ //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example:
+ // Groups[0].Properties[2].Alias
+ serverValidationManager.addFieldError(e, modelState[e][0]);
}
//add to notifications
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js
index 75735abe41..805ffd33ea 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js
@@ -505,11 +505,19 @@ function umbDataFormatter() {
saveModel.groups = _.map(realGroups, function (g) {
var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name');
- var saveProperties = _.map(g.properties, function(p) {
+
+ var realProperties = _.reject(g.properties, function (p) {
+ //do not include these properties
+ return p.propertyState === "init";
+ });
+
+ var saveProperties = _.map(realProperties, function (p) {
var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId');
return saveProperty;
});
+ saveGroup.properties = saveProperties;
+
return saveGroup;
});
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html
index 231e642887..3d02980e97 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html
@@ -29,7 +29,7 @@
-
+
+
+
{{ property.alias }}
-
{{ property.alias }}
+
+
+
-
-
-
-
-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js
index 3de552eccc..050476090a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js
@@ -180,11 +180,8 @@
saveMethod: contentTypeResource.save,
scope: $scope,
content: vm.contentType,
- rebindCallback: function (origContent, savedContent) {
- //This is called when the data returns and we need to merge the property values from the saved content to the original content.
- //re-binds all changed property values to the origContent object from the savedContent object and returns an array of changed properties.
-
- }
+ //no-op for rebind callback... we don't really need to rebind for content types
+ rebindCallback: angular.noop
}).then(function (data) {
//success
syncTreeNode(vm.contentType, data.path);
diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js
index 0fa55c2a11..73772ae2a9 100644
--- a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js
+++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js
@@ -108,7 +108,7 @@ describe('contentEditingHelper tests', function () {
var allProps = contentEditingHelper.getAllProps(content);
//act
- formHelper.handleServerValidation({ "Property.bodyText": ["Required"] });
+ formHelper.handleServerValidation({ "_Properties.bodyText": ["Required"] });
//assert
expect(serverValidationManager.items.length).toBe(1);
@@ -124,7 +124,7 @@ describe('contentEditingHelper tests', function () {
var allProps = contentEditingHelper.getAllProps(content);
//act
- formHelper.handleServerValidation({ "Property.bodyText.value": ["Required"] });
+ formHelper.handleServerValidation({ "_Properties.bodyText.value": ["Required"] });
//assert
expect(serverValidationManager.items.length).toBe(1);
@@ -144,8 +144,8 @@ describe('contentEditingHelper tests', function () {
{
"Name": ["Required"],
"UpdateDate": ["Invalid date"],
- "Property.bodyText.value": ["Required field"],
- "Property.textarea": ["Invalid format"]
+ "_Properties.bodyText.value": ["Required field"],
+ "_Properties.textarea": ["Invalid format"]
});
//assert
diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs
index 9bd0ba50fd..dc47d15234 100644
--- a/src/Umbraco.Web/Editors/ContentTypeController.cs
+++ b/src/Umbraco.Web/Editors/ContentTypeController.cs
@@ -16,6 +16,7 @@ using Newtonsoft.Json;
using Umbraco.Core.PropertyEditors;
using System;
using System.Net.Http;
+using Umbraco.Core.Services;
namespace Umbraco.Web.Editors
{
@@ -118,6 +119,8 @@ namespace Umbraco.Web.Editors
//TODO: This all needs to be done in a transaction!!
// Which means that all of this logic needs to take place inside the service
+ ContentTypeDisplay display;
+
if (ctId > 0)
{
//its an update to an existing
@@ -127,8 +130,8 @@ namespace Umbraco.Web.Editors
Mapper.Map(contentTypeSave, found);
ctService.Save(found);
-
- return Mapper.Map(found);
+
+ display = Mapper.Map(found);
}
else
{
@@ -171,9 +174,15 @@ namespace Umbraco.Web.Editors
newCt.AddContentType(newCt);
ctService.Save(newCt);
}
-
- return Mapper.Map(newCt);
+
+ display = Mapper.Map(newCt);
}
+
+ display.AddSuccessNotification(
+ Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"),
+ string.Empty);
+
+ return display;
}
///
diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
index 2ece1f21d8..ea652dbe50 100644
--- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
+++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs
@@ -14,6 +14,7 @@ using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
+using Umbraco.Web.WebApi;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Editors
@@ -22,6 +23,7 @@ namespace Umbraco.Web.Editors
/// Am abstract API controller providing functionality used for dealing with content and media types
///
[PluginController("UmbracoApi")]
+ [PrefixlessBodyModelValidator]
public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController
{
private ICultureDictionary _cultureDictionary;
diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs
index 08868e4e5f..5df479ce76 100644
--- a/src/Umbraco.Web/ModelStateExtensions.cs
+++ b/src/Umbraco.Web/ModelStateExtensions.cs
@@ -64,7 +64,7 @@ namespace Umbraco.Web
if (!result.MemberNames.Any())
{
//add a model state error for the entire property
- modelState.AddModelError(string.Format("{0}.{1}", "Properties", propertyAlias), result.ErrorMessage);
+ modelState.AddModelError(string.Format("{0}.{1}", "_Properties", propertyAlias), result.ErrorMessage);
}
else
{
@@ -72,7 +72,7 @@ namespace Umbraco.Web
// so that we can try to match it up to a real sub field of this editor
foreach (var field in result.MemberNames)
{
- modelState.AddModelError(string.Format("{0}.{1}.{2}", "Properties", propertyAlias, field), result.ErrorMessage);
+ modelState.AddModelError(string.Format("{0}.{1}.{2}", "_Properties", propertyAlias, field), result.ErrorMessage);
}
}
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs
index 13f8235f69..f138083143 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs
@@ -26,9 +26,11 @@ namespace Umbraco.Web.Models.ContentEditing
public override string Alias { get; set; }
[DataMember(Name = "updateDate")]
+ [ReadOnly(true)]
public DateTime UpdateDate { get; set; }
[DataMember(Name = "createDate")]
+ [ReadOnly(true)]
public DateTime CreateDate { get; set; }
[DataMember(Name = "description")]
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs
index 16ff968cee..7a3869d9b1 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs
@@ -11,7 +11,7 @@ using Umbraco.Core.Models.Validation;
namespace Umbraco.Web.Models.ContentEditing
{
[DataContract(Name = "contentType", Namespace = "")]
- public class ContentTypeCompositionDisplay : ContentTypeBasic
+ public class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel
{
public ContentTypeCompositionDisplay()
{
diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs
index 4332a93436..ce7be6dd84 100644
--- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs
@@ -26,15 +26,17 @@ namespace Umbraco.Web.Models.ContentEditing
public PropertyTypeValidation Validation { get; set; }
[DataMember(Name = "label")]
+ [Required]
public string Label { get; set; }
[DataMember(Name = "sortOrder")]
public int SortOrder { get; set; }
[DataMember(Name = "dataTypeId")]
+ [Required]
public int DataTypeId { get; set; }
- //SD: Is this really required ?
+ //SD: Is this really needed ?
[DataMember(Name = "groupId")]
public int GroupId { get; set; }
}
diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs
index 3c7f0d7022..75bd6bbaae 100644
--- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs
+++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs
@@ -225,6 +225,8 @@ namespace Umbraco.Web.Models.Mapping
#region *** Used for mapping on top of an existing display object from a save object ***
config.CreateMap()
+ .ForMember(dto => dto.CreateDate, expression => expression.Ignore())
+ .ForMember(dto => dto.UpdateDate, expression => expression.Ignore())
.ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore())
.ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore())
.ForMember(dto => dto.ListViewEditorName, expression => expression.Ignore())
diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs
index 96b286b8fe..9d9457eb50 100644
--- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs
+++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs
@@ -68,6 +68,10 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(dto => dto.Id, expression => expression.Condition(display => (Convert.ToInt32(display.Id) > 0)))
.ForMember(dto => dto.Id, expression => expression.MapFrom(display => Convert.ToInt32(display.Id)))
+ //These get persisted as part of the saving procedure, nothing to do with the display model
+ .ForMember(dto => dto.CreateDate, expression => expression.Ignore())
+ .ForMember(dto => dto.UpdateDate, expression => expression.Ignore())
+
.ForMember(dto => dto.AllowedAsRoot, expression => expression.MapFrom(display => display.AllowAsRoot))
.ForMember(dto => dto.CreatorId, expression => expression.Ignore())
.ForMember(dto => dto.Level, expression => expression.Ignore())
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 1f1f5585c7..c7864b2204 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -904,6 +904,8 @@
+
+
diff --git a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs
new file mode 100644
index 0000000000..c5563e6509
--- /dev/null
+++ b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Validation;
+
+namespace Umbraco.Web.WebApi
+{
+ ///
+ /// By default WebApi always appends a prefix to any ModelState error but we don't want this,
+ /// so this is a custom validator that ensures there is no prefix set.
+ ///
+ ///
+ /// We were already doing this with the content/media/members validation since we had to manually validate because we
+ /// were posting multi-part values. We were always passing in an empty prefix so it worked. However for other editors we
+ /// are validating with normal data annotations (for the most part) and we don't want the prefix there either.
+ ///
+ internal class PrefixlessBodyModelValidator : IBodyModelValidator
+ {
+ private readonly IBodyModelValidator _innerValidator;
+
+ public PrefixlessBodyModelValidator(IBodyModelValidator innerValidator)
+ {
+ if (innerValidator == null)
+ {
+ throw new ArgumentNullException("innerValidator");
+ }
+
+ _innerValidator = innerValidator;
+ }
+
+ public bool Validate(object model, Type type, ModelMetadataProvider metadataProvider, HttpActionContext actionContext, string keyPrefix)
+ {
+ // Remove the keyPrefix but otherwise let innerValidator do what it normally does.
+ return _innerValidator.Validate(model, type, metadataProvider, actionContext, string.Empty);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs
new file mode 100644
index 0000000000..e018881f8a
--- /dev/null
+++ b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Validation;
+
+namespace Umbraco.Web.WebApi
+{
+ ///
+ /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention.
+ ///
+ internal class PrefixlessBodyModelValidatorAttribute : Attribute, IControllerConfiguration
+ {
+ public virtual void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
+ {
+ //replace the normal validator with our custom one for this controller
+ controllerSettings.Services.Replace(typeof(IBodyModelValidator),
+ new PrefixlessBodyModelValidator(controllerSettings.Services.GetBodyModelValidator()));
+ }
+ }
+}
\ No newline at end of file