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 @@
- +
+ required + val-server-field="{{'Groups[' + $index + '].Name'}}" />
@@ -90,19 +91,22 @@ Inherited from {{property.contentTypeName}}
-
+ +
+
{{ 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