From d28cdf3bdc3a3c66b8eeb55974dfb4e900c8bdb3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 17 Oct 2013 11:28:21 +1100 Subject: [PATCH] Updates the formHelper and streamlines the way form submission is handled, updates the member and user controller to use the new change password functionality in the Security object which uses all of the correct logic to update a password based on the membership provider. Gets the user's change password form working correctly with server validation. now just need to get the user membership provider to actually work. --- .../src/common/resources/user.resource.js | 13 +- .../services/contenteditinghelper.service.js | 59 +------- .../src/common/services/formhelper.service.js | 82 ++++++++++- .../services/servervalidationmgr.service.js | 7 +- .../src/views/dashboard/ChangePassword.html | 28 +--- .../dashboard/dashboard.tabs.controller.js | 25 ++-- .../services/content-editing-helper.spec.js | 12 +- src/Umbraco.Web/Editors/MemberController.cs | 131 ++---------------- src/Umbraco.Web/Editors/UserController.cs | 41 ++++-- ...swordModel.cs => ChangingPasswordModel.cs} | 21 ++- .../Models/ContentEditing/MemberSave.cs | 4 +- src/Umbraco.Web/Security/WebSecurity.cs | 118 +++++++++++++++- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 13 files changed, 285 insertions(+), 258 deletions(-) rename src/Umbraco.Web/Models/ContentEditing/{ChangePasswordModel.cs => ChangingPasswordModel.cs} (67%) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/user.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/user.resource.js index b0c338fa90..857ef65160 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/user.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/user.resource.js @@ -77,26 +77,17 @@ function userResource($q, $http, umbRequestHelper) { * * @description * Changes the current users password - * - * ##usage - *
-         * contentResource.getAll()
-         *    .then(function(userArray) {
-         *        var myUsers = userArray; 
-         *        alert('they are here!');
-         *    });
-         * 
* * @returns {Promise} resourcePromise object containing the user array. * */ - changePassword: function (oldPassword, newPassword) { + changePassword: function (changePasswordArgs) { return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "userApiBaseUrl", "PostChangePassword"), - { oldPassword: oldPassword, newPassword: newPassword }), + changePasswordArgs), 'Failed to change password'); }, 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 28f8679d1a..4dec2c17b9 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 @@ -4,7 +4,7 @@ * @name umbraco.services.contentEditingHelper * @description A helper service for content/media/member controllers when editing/creating/saving content. **/ -function contentEditingHelper($location, $routeParams, notificationsService, serverValidationManager, dialogService) { +function contentEditingHelper($location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper) { return { @@ -95,60 +95,6 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser return changed; }, - /** - * @ngdoc method - * @name umbraco.services.contentEditingHelper#handleValidationErrors - * @methodOf umbraco.services.contentEditingHelper - * @function - * - * @description - * A function to handle the validation (modelState) errors collection which will happen on a 400 error indicating validation errors - * It's worth noting that when a 400 occurs, the data is still saved just never published, though this depends on if the entity is a new - * entity and whether or not the data fulfils the absolute basic requirements like having a mandatory Name. - */ - handleValidationErrors: function (allProps, modelState) { - - //find the content property for the current error, for use in the loop below - function findContentProp(props, propAlias) { - return _.find(props, function (item) { - return (item.alias === propAlias); - }); - } - - 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 = findContentProp(allProps, 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 - serverValidationManager.addPropertyError(contentProperty.alias, parts[2], modelState[e][0]); - } - else { - //add a generic error for the property, no reference to a specific field - serverValidationManager.addPropertyError(contentProperty.alias, "", modelState[e][0]); - } - } - } - else { - //the parts are only 1, this means its not a property but a native content property - serverValidationManager.addFieldError(parts[0], modelState[e][0]); - } - - //add to notifications - notificationsService.error("Validation", modelState[e][0]); - } - }, - /** * @ngdoc function * @name umbraco.services.contentEditingHelper#handleSaveError @@ -174,7 +120,8 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser //now we need to look through all the validation errors if (args.err.data && (args.err.data.ModelState)) { - this.handleValidationErrors(args.allNewProps, args.err.data.ModelState); + //wire up the server validation errs + formHelper.handleServerValidation(args.err.data.ModelState); if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) { //we are not redirecting because this is not new content, it is existing content. In this case 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 7efc8bebaa..647f124be6 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 @@ -7,7 +7,7 @@ * A utility class used to streamline how forms are developed, to ensure that validation is check and displayed consistently and to ensure that the correct events * fire when they need to. */ -function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService) { +function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService) { return { /** @@ -102,6 +102,86 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati } args.scope.$broadcast("formSubmitted", { scope: args.scope }); + }, + + /** + * @ngdoc function + * @name umbraco.services.formHelper#handleError + * @methodOf umbraco.services.formHelper + * @function + * + * @description + * Needs to be called when a form submission fails, this will wire up all server validation errors in ModelState and + * add the correct messages to the notifications. If a server error has occurred this will show a ysod. + * + * @param {object} err The error object returned from the http promise + */ + handleError: function (err) { + //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, 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 === 400) { + //now we need to look through all the validation errors + if (err.data && (err.data.ModelState)) { + + //wire up the server validation errs + this.handleServerValidation(err.data.ModelState); + + //execute all server validation events and subscribers + serverValidationManager.executeAndClearAllSubscriptions(); + } + else { + dialogService.ysodDialog(err); + } + } + else { + dialogService.ysodDialog(err); + } + }, + + /** + * @ngdoc function + * @name umbraco.services.formHelper#handleServerValidation + * @methodOf umbraco.services.formHelper + * @function + * + * @description + * This wires up all of the server validation model state so that valServer and valServerField directives work + * + * @param {object} err The error object returned from the http promise + */ + handleServerValidation: function(modelState) { + 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 for properties since all model errors for properties are prefixed with "Properties" + //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) { + var propertyAlias = parts[1]; + + //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 + serverValidationManager.addPropertyError(propertyAlias, parts[2], modelState[e][0]); + } + else { + //add a generic error for the property, no reference to a specific field + serverValidationManager.addPropertyError(propertyAlias, "", modelState[e][0]); + } + + } + else { + //the parts are only 1, this means its not a property but a native content property + serverValidationManager.addFieldError(parts[0], modelState[e][0]); + } + + //add to notifications + notificationsService.error("Validation", modelState[e][0]); + } } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index b8d97e374c..03f5e96ab0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -44,8 +44,11 @@ function serverValidationManager($timeout) { * @function * * @description - * This is primarily used for scenarios where the error collection needs to be persisted over a route change. Generally this - * is when a content item (or any item) is created. The controller should call this method once the data is bound to the scope + * This method needs to be called once all field and property errors are wired up. + * + * In some scenarios where the error collection needs to be persisted over a route change + * (i.e. when a content item (or any item) is created and the route redirects to the editor) + * the controller should call this method once the data is bound to the scope * so that any persisted validation errors are re-bound to their controls. Once they are re-binded this then clears the validation * colleciton so that if another route change occurs, the previously persisted validation errors are not re-bound to the new item. */ diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/ChangePassword.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/ChangePassword.html index 54d4827240..9d56075962 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/ChangePassword.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/ChangePassword.html @@ -1,6 +1,6 @@

Change password

@@ -9,32 +9,10 @@ - - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js index 64040362d4..b113fdf93e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js @@ -84,9 +84,10 @@ function ChangePasswordDashboardController($scope, xmlhelper, $log, userResource //create the initial model for change password property editor $scope.changePasswordModel = { - alias: "password", + alias: "_umb_password", view: "changepassword", - config: {} + config: {}, + value: {} }; //go get the config for the membership provider and add it to the model @@ -101,20 +102,18 @@ function ChangePasswordDashboardController($scope, xmlhelper, $log, userResource ////this is the model we will pass to the service //$scope.profile = {}; - $scope.changePassword = function(p) { + $scope.changePassword = function() { if (formHelper.submitForm({ scope: $scope })) { - //userResource.changePassword(p.oldPassword, p.newPassword).then(function() { + userResource.changePassword($scope.changePasswordModel.value).then(function() { - // formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - // //TODO: This is temporary - server validation will work automatically with the val-server directives. - // $scope.passwordForm.$setValidity(true); - //}, function () { - // //TODO: This is temporary - server validation will work automatically with the val-server directives. - // //this only happens if there is a wrong oldPassword sent along - // $scope.passwordForm.oldpass.$setValidity("oldPassword", false); - //}); + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + }, function (err) { + + formHelper.handleError(err); + + }); } }; } 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 2c73044d3e..1fdd7aee62 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 @@ -1,5 +1,5 @@ describe('contentEditingHelper tests', function () { - var contentEditingHelper, $routeParams, serverValidationManager, mocksUtils, notificationsService; + var contentEditingHelper, $routeParams, serverValidationManager, mocksUtils, notificationsService, formHelper; beforeEach(module('umbraco.services')); beforeEach(module('umbraco.mocks')); @@ -14,6 +14,7 @@ describe('contentEditingHelper tests', function () { serverValidationManager = $injector.get('serverValidationManager'); mocksUtils = $injector.get('mocksUtils'); notificationsService = $injector.get('notificationsService'); + formHelper = $injector.get('formHelper'); })); describe('handles http validation errors', function () { @@ -88,7 +89,7 @@ describe('contentEditingHelper tests', function () { var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(allProps, { Name: ["Required"] }); + formHelper.handleServerValidation({ Name: ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -104,7 +105,7 @@ describe('contentEditingHelper tests', function () { var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(allProps, { "Property.bodyText": ["Required"] }); + formHelper.handleServerValidation({ "Property.bodyText": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -120,7 +121,7 @@ describe('contentEditingHelper tests', function () { var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(allProps, { "Property.bodyText.value": ["Required"] }); + formHelper.handleServerValidation({ "Property.bodyText.value": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -136,8 +137,7 @@ describe('contentEditingHelper tests', function () { var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors( - allProps, + formHelper.handleServerValidation( { "Name": ["Required"], "UpdateDate": ["Invalid date"], diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index fb36cfeca6..bd8895d3a0 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -250,132 +251,18 @@ namespace Umbraco.Web.Editors //password changes ? if (contentItem.Password == null) return null; - //Are we resetting the password?? - if (contentItem.Password.Reset.HasValue && contentItem.Password.Reset.Value) + var passwordChangeResult = Security.ChangePassword(membershipUser.UserName, contentItem.Password, Membership.Provider); + if (passwordChangeResult.Success) { - if (Membership.Provider.EnablePasswordReset == false) - { - ModelState.AddPropertyError( - new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - else if (Membership.Provider.RequiresQuestionAndAnswer && contentItem.Password.Answer.IsNullOrWhiteSpace()) - { - ModelState.AddPropertyError( - new ValidationResult("Password reset requires a password answer", new[] {"resetPassword"}), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - else - { - //ok, we should be able to reset it - try - { - var newPass = Membership.Provider.ResetPassword( - membershipUser.UserName, - Membership.Provider.RequiresQuestionAndAnswer ? contentItem.Password.Answer : null); - - //return the generated pword - return newPass; - } - catch (Exception ex) - { - LogHelper.WarnWithException("Could not reset member password", ex); - ModelState.AddPropertyError( - new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] {"resetPassword"}), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - } + //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword + return passwordChangeResult.Result.ResetPassword; } - else if (contentItem.Password.NewPassword.IsNullOrWhiteSpace() == false) - { - //we're not resetting it so we need to try to change it. - if (contentItem.Password.OldPassword.IsNullOrWhiteSpace() && Membership.Provider.EnablePasswordRetrieval == false) - { - //if password retrieval is not enabled but there is no old password we cannot continue + //it wasn't successful, so add the change error to the model state + ModelState.AddPropertyError( + passwordChangeResult.Result.ChangeError, + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - ModelState.AddPropertyError( - new ValidationResult("Password cannot be changed without the old password", new[] {"value"}), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - else if (contentItem.Password.OldPassword.IsNullOrWhiteSpace() == false) - { - //if an old password is suplied try to change it - - try - { - var result = Membership.Provider.ChangePassword(membershipUser.UserName, contentItem.Password.OldPassword, contentItem.Password.NewPassword); - if (result == false) - { - ModelState.AddPropertyError( - new ValidationResult("Could not change password", new[] {"value"}), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - } - catch (Exception ex) - { - LogHelper.WarnWithException("Could not change member password", ex); - ModelState.AddPropertyError( - new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] {"value"}), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - } - else if (Membership.Provider.EnablePasswordRetrieval == false) - { - //we cannot continue if we cannot get the current password - - ModelState.AddPropertyError( - new ValidationResult("Password cannot be changed without the old password", new[] {"value"}), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - else if (Membership.Provider.RequiresQuestionAndAnswer && contentItem.Password.Answer.IsNullOrWhiteSpace()) - { - //if the question answer is required but there isn't one, we cannot continue - - ModelState.AddPropertyError( - new ValidationResult("Password cannot be changed without the password answer", new[] {"value"}), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - - } - else - { - //lets try to get the old one so we can change it - - try - { - var oldPassword = Membership.Provider.GetPassword( - membershipUser.UserName, - Membership.Provider.RequiresQuestionAndAnswer ? contentItem.Password.Answer : null); - - try - { - var result = Membership.Provider.ChangePassword(membershipUser.UserName, oldPassword, contentItem.Password.NewPassword); - if (result == false) - { - ModelState.AddPropertyError( - new ValidationResult("Could not change password", new[] {"value"}), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - } - catch (Exception ex1) - { - LogHelper.WarnWithException("Could not change member password", ex1); - ModelState.AddPropertyError( - new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - - } - catch (Exception ex2) - { - LogHelper.WarnWithException("Could not retrieve member password", ex2); - ModelState.AddPropertyError( - new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - } - - } - } return null; } diff --git a/src/Umbraco.Web/Editors/UserController.cs b/src/Umbraco.Web/Editors/UserController.cs index 31944dcc62..5014c53eb3 100644 --- a/src/Umbraco.Web/Editors/UserController.cs +++ b/src/Umbraco.Web/Editors/UserController.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Configuration; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; using legacyUser = umbraco.BusinessLogic.User; using System.Net.Http; using System.Collections.Specialized; @@ -60,18 +61,36 @@ namespace Umbraco.Web.Editors /// Changes the users password /// /// - /// - public HttpResponseMessage PostChangePassword(ChangePasswordModel data) - { - - var u = UmbracoContext.Security.CurrentUser; - if (!UmbracoContext.Security.ValidateBackOfficeCredentials(u.Username, data.OldPassword)) - return new HttpResponseMessage(HttpStatusCode.Forbidden); + /// + /// If the password is being reset it will return the newly reset password, otherwise will return null; + /// + public string PostChangePassword(ChangingPasswordModel data) + { + var userProvider = Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider]; + if (userProvider == null) + { + throw new InvalidOperationException("No membership provider found with the name " + UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider); + } - if(!UmbracoContext.Security.ChangePassword(data.OldPassword, data.NewPassword)) - return new HttpResponseMessage(HttpStatusCode.InternalServerError); - - return new HttpResponseMessage(HttpStatusCode.OK); + //TODO: WE need to support this! - requires UI updates, etc... + if (userProvider.RequiresQuestionAndAnswer) + { + throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified"); + } + + var passwordChangeResult = Security.ChangePassword(Security.CurrentUser.Username, data, userProvider); + if (passwordChangeResult.Success) + { + //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword + return passwordChangeResult.Result.ResetPassword; + } + + //it wasn't successful, so add the change error to the model state + ModelState.AddPropertyError( + passwordChangeResult.Result.ChangeError, + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } /// diff --git a/src/Umbraco.Web/Models/ContentEditing/ChangePasswordModel.cs b/src/Umbraco.Web/Models/ContentEditing/ChangingPasswordModel.cs similarity index 67% rename from src/Umbraco.Web/Models/ContentEditing/ChangePasswordModel.cs rename to src/Umbraco.Web/Models/ContentEditing/ChangingPasswordModel.cs index e4876b0d00..362894ca42 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ChangePasswordModel.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ChangingPasswordModel.cs @@ -1,11 +1,28 @@ -using System.Runtime.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing { + /// + /// A model representing an attempt at changing a password + /// + public class PasswordChangedModel + { + /// + /// The error affiliated with the failing password changes, null if changing was successful + /// + public ValidationResult ChangeError { get; set; } + + /// + /// If the password was reset, this is the value it has been changed to + /// + public string ResetPassword { get; set; } + } + /// /// A model representing the data required to set a member/user password depending on the provider installed. /// - public class ChangePasswordModel + public class ChangingPasswordModel { /// /// The password value diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs b/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs index 2826b53869..ba2d91ab23 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Models.ContentEditing { public MemberSave() { - Password = new ChangePasswordModel(); + Password = new ChangingPasswordModel(); } [DataMember(Name = "username", IsRequired = true)] @@ -24,6 +24,6 @@ namespace Umbraco.Web.Models.ContentEditing public string Email { get; set; } [DataMember(Name = "password")] - public ChangePasswordModel Password { get; set; } + public ChangingPasswordModel Password { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 7cef53ef78..5b236da7f1 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web; using System.Web.Security; @@ -10,6 +11,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; +using Umbraco.Web.Models.ContentEditing; using umbraco; using umbraco.DataLayer; using umbraco.businesslogic.Exceptions; @@ -190,15 +192,119 @@ namespace Umbraco.Web.Security } /// - /// Changes password for a back office user + /// Changes password for a member/user given the membership provider and the password change model /// - /// - /// + /// + /// + /// /// - internal bool ChangePassword(string oldpassword, string newpassword) + /// + /// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes! + /// + internal Attempt ChangePassword(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider) { - var membershipProvider = Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider]; - return membershipProvider.GetUser(CurrentUser.Username, true).ChangePassword(oldpassword, newpassword); + if (passwordModel == null) throw new ArgumentNullException("passwordModel"); + if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); + + //Are we resetting the password?? + if (passwordModel.Reset.HasValue && passwordModel.Reset.Value) + { + if (membershipProvider.EnablePasswordReset == false) + { + return Attempt.Fail(new PasswordChangedModel {ChangeError = new ValidationResult("Password reset is not enabled", new[] {"resetPassword"})}); + } + if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace()) + { + return Attempt.Fail(new PasswordChangedModel {ChangeError = new ValidationResult("Password reset requires a password answer", new[] {"resetPassword"})}); + } + //ok, we should be able to reset it + try + { + var newPass = membershipProvider.ResetPassword( + username, + membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null); + + //return the generated pword + return Attempt.Succeed(new PasswordChangedModel {ResetPassword = newPass}); + } + catch (Exception ex) + { + LogHelper.WarnWithException("Could not reset member password", ex); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] { "resetPassword" }) }); + } + } + if (passwordModel.NewPassword.IsNullOrWhiteSpace() == false) + { + //we're not resetting it so we need to try to change it. + + if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false) + { + //if password retrieval is not enabled but there is no old password we cannot continue + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "value" }) }); + } + if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false) + { + //if an old password is suplied try to change it + + try + { + var result = membershipProvider.ChangePassword(username, passwordModel.OldPassword, passwordModel.NewPassword); + if (result == false) + { + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) }); + } + } + catch (Exception ex) + { + LogHelper.WarnWithException("Could not change member password", ex); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) }); + } + } + else if (membershipProvider.EnablePasswordRetrieval == false) + { + //we cannot continue if we cannot get the current password + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "value" }) }); + } + else if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace()) + { + //if the question answer is required but there isn't one, we cannot continue + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the password answer", new[] { "value" }) }); + } + else + { + //lets try to get the old one so we can change it + + try + { + var oldPassword = membershipProvider.GetPassword( + username, + membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null); + + try + { + var result = membershipProvider.ChangePassword(username, oldPassword, passwordModel.NewPassword); + if (result == false) + { + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password", new[] { "value" }) }); + } + } + catch (Exception ex1) + { + LogHelper.WarnWithException("Could not change member password", ex1); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }) }); + } + + } + catch (Exception ex2) + { + LogHelper.WarnWithException("Could not retrieve member password", ex2); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }) }); + } + } + } + + //woot! + return Attempt.Succeed(new PasswordChangedModel()); } /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d545ee8365..3ca7c79119 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -313,7 +313,7 @@ - +