Просмотр исходного кода

authenticated token updates and fixes

Jason Rivard 7 лет назад
Родитель
Сommit
27ecff3971
31 измененных файлов с 1926 добавлено и 1890 удалено
  1. 21 0
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  2. 0 71
      server/src/main/java/password/pwm/bean/TokenVerificationProgress.java
  3. 5 5
      server/src/main/java/password/pwm/config/Configuration.java
  4. 4 4
      server/src/main/java/password/pwm/config/profile/UpdateProfileProfile.java
  5. 3 3
      server/src/main/java/password/pwm/http/SessionManager.java
  6. 10 7
      server/src/main/java/password/pwm/http/bean/NewUserBean.java
  7. 8 8
      server/src/main/java/password/pwm/http/bean/UpdateProfileBean.java
  8. 2 1
      server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java
  9. 0 880
      server/src/main/java/password/pwm/http/servlet/UpdateProfileServlet.java
  10. 16 13
      server/src/main/java/password/pwm/http/servlet/activation/ActivateUserServlet.java
  11. 26 17
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  12. 64 80
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java
  13. 155 184
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  14. 453 0
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileServlet.java
  15. 407 0
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java
  16. 6 6
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  17. 4 4
      server/src/main/java/password/pwm/svc/token/TokenPayload.java
  18. 13 10
      server/src/main/java/password/pwm/svc/token/TokenService.java
  19. 2 4
      server/src/main/java/password/pwm/svc/token/TokenType.java
  20. 97 12
      server/src/main/java/password/pwm/svc/token/TokenUtil.java
  21. 28 0
      server/src/main/java/password/pwm/util/form/FormUtility.java
  22. 14 0
      server/src/main/java/password/pwm/util/java/StringUtil.java
  23. 11 0
      server/src/main/java/password/pwm/util/macro/MacroMachine.java
  24. 11 11
      server/src/main/java/password/pwm/ws/server/rest/RestProfileServer.java
  25. 0 1
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  26. 1 6
      server/src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp
  27. 1 0
      server/src/main/webapp/WEB-INF/jsp/configeditor.jsp
  28. 6 11
      server/src/main/webapp/WEB-INF/jsp/newuser-entercode.jsp
  29. 7 23
      server/src/main/webapp/WEB-INF/jsp/updateprofile-entercode.jsp
  30. 551 0
      server/src/main/webapp/public/resources/js/configeditor-settings-form.js
  31. 0 529
      server/src/main/webapp/public/resources/js/configeditor-settings.js

+ 21 - 0
server/src/main/java/password/pwm/bean/TokenDestinationItem.java

@@ -26,6 +26,7 @@ import lombok.Builder;
 import lombok.Value;
 import password.pwm.PwmApplication;
 import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.UserInfo;
@@ -36,6 +37,7 @@ import password.pwm.util.secure.SecureService;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -44,6 +46,25 @@ import java.util.Map;
 @Builder
 public class TokenDestinationItem implements Serializable
 {
+    private static final Map<PwmSetting, TokenDestinationItem.Type> SETTING_TO_DEST_TYPE_MAP;
+
+    static
+    {
+        final Map<PwmSetting, TokenDestinationItem.Type> tempMap = new HashMap<>(  );
+        tempMap.put( PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE, TokenDestinationItem.Type.email );
+        tempMap.put( PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE_2, TokenDestinationItem.Type.email );
+        tempMap.put( PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE_3, TokenDestinationItem.Type.email );
+        tempMap.put( PwmSetting.SMS_USER_PHONE_ATTRIBUTE, TokenDestinationItem.Type.sms );
+        tempMap.put( PwmSetting.SMS_USER_PHONE_ATTRIBUTE_2, TokenDestinationItem.Type.sms );
+        tempMap.put( PwmSetting.SMS_USER_PHONE_ATTRIBUTE_3, TokenDestinationItem.Type.sms );
+        SETTING_TO_DEST_TYPE_MAP = Collections.unmodifiableMap( tempMap );
+    }
+
+    public static Map<PwmSetting, Type> getSettingToDestTypeMap( )
+    {
+        return SETTING_TO_DEST_TYPE_MAP;
+    }
+
     private String id;
     private String display;
     private String value;

+ 0 - 71
server/src/main/java/password/pwm/bean/TokenVerificationProgress.java

@@ -1,71 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.bean;
-
-import java.io.Serializable;
-import java.util.HashSet;
-import java.util.Set;
-
-public class TokenVerificationProgress implements Serializable
-{
-    private Set<TokenChannel> passedTokens = new HashSet<>();
-    private Set<TokenChannel> issuedTokens = new HashSet<>();
-    private TokenChannel phase;
-    private String tokenDisplayText;
-
-    public enum TokenChannel
-    {
-        EMAIL,
-        SMS,
-    }
-
-    public Set<TokenChannel> getPassedTokens( )
-    {
-        return passedTokens;
-    }
-
-    public Set<TokenChannel> getIssuedTokens( )
-    {
-        return issuedTokens;
-    }
-
-    public TokenChannel getPhase( )
-    {
-        return phase;
-    }
-
-    public void setPhase( final TokenChannel phase )
-    {
-        this.phase = phase;
-    }
-
-    public String getTokenDisplayText( )
-    {
-        return tokenDisplayText;
-    }
-
-    public void setTokenDisplayText( final String tokenDisplayText )
-    {
-        this.tokenDisplayText = tokenDisplayText;
-    }
-}

+ 5 - 5
server/src/main/java/password/pwm/config/Configuration.java

@@ -44,7 +44,7 @@ import password.pwm.config.profile.ProfileUtility;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.profile.SetupOtpProfile;
-import password.pwm.config.profile.UpdateAttributesProfile;
+import password.pwm.config.profile.UpdateProfileProfile;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.config.stored.StoredConfigurationUtil;
@@ -1108,13 +1108,13 @@ public class Configuration implements SettingReader
         return returnMap;
     }
 
-    public Map<String, UpdateAttributesProfile> getUpdateAttributesProfile( )
+    public Map<String, UpdateProfileProfile> getUpdateAttributesProfile( )
     {
-        final Map<String, UpdateAttributesProfile> returnMap = new LinkedHashMap<>();
+        final Map<String, UpdateProfileProfile> returnMap = new LinkedHashMap<>();
         final Map<String, Profile> profileMap = profileMap( ProfileType.UpdateAttributes );
         for ( final Map.Entry<String, Profile> entry : profileMap.entrySet() )
         {
-            returnMap.put( entry.getKey(), ( UpdateAttributesProfile ) entry.getValue() );
+            returnMap.put( entry.getKey(), ( UpdateProfileProfile ) entry.getValue() );
         }
         return returnMap;
     }
@@ -1162,7 +1162,7 @@ public class Configuration implements SettingReader
                 break;
 
             case UpdateAttributes:
-                newProfile = UpdateAttributesProfile.makeFromStoredConfiguration( storedConfiguration, profileID );
+                newProfile = UpdateProfileProfile.makeFromStoredConfiguration( storedConfiguration, profileID );
                 break;
 
             case DeleteAccount:

+ 4 - 4
server/src/main/java/password/pwm/config/profile/UpdateAttributesProfile.java → server/src/main/java/password/pwm/config/profile/UpdateProfileProfile.java

@@ -32,20 +32,20 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
-public class UpdateAttributesProfile extends AbstractProfile implements Profile
+public class UpdateProfileProfile extends AbstractProfile implements Profile
 {
 
     private static final ProfileType PROFILE_TYPE = ProfileType.UpdateAttributes;
 
-    protected UpdateAttributesProfile( final String identifier, final Map<PwmSetting, StoredValue> storedValueMap )
+    protected UpdateProfileProfile( final String identifier, final Map<PwmSetting, StoredValue> storedValueMap )
     {
         super( identifier, storedValueMap );
     }
 
-    public static UpdateAttributesProfile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final String identifier )
+    public static UpdateProfileProfile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final String identifier )
     {
         final Map<PwmSetting, StoredValue> valueMap = makeValueMap( storedConfiguration, identifier, PROFILE_TYPE.getCategory() );
-        return new UpdateAttributesProfile( identifier, valueMap );
+        return new UpdateProfileProfile( identifier, valueMap );
 
     }
 

+ 3 - 3
server/src/main/java/password/pwm/http/SessionManager.java

@@ -34,7 +34,7 @@ import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.Profile;
 import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.SetupOtpProfile;
-import password.pwm.config.profile.UpdateAttributesProfile;
+import password.pwm.config.profile.UpdateProfileProfile;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -260,9 +260,9 @@ public class SessionManager
         return ( SetupOtpProfile ) getProfile( pwmApplication, ProfileType.SetupOTPProfile );
     }
 
-    public UpdateAttributesProfile getUpdateAttributeProfile( final PwmApplication pwmApplication ) throws PwmUnrecoverableException
+    public UpdateProfileProfile getUpdateAttributeProfile( final PwmApplication pwmApplication ) throws PwmUnrecoverableException
     {
-        return ( UpdateAttributesProfile ) getProfile( pwmApplication, ProfileType.UpdateAttributes );
+        return ( UpdateProfileProfile ) getProfile( pwmApplication, ProfileType.UpdateAttributes );
     }
 
     public DeleteAccountProfile getSelfDeleteProfile( final PwmApplication pwmApplication ) throws PwmUnrecoverableException

+ 10 - 7
server/src/main/java/password/pwm/http/bean/NewUserBean.java

@@ -26,7 +26,6 @@ import com.google.gson.annotations.SerializedName;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
-import password.pwm.bean.TokenVerificationProgress;
 import password.pwm.config.option.SessionBeanMode;
 import password.pwm.http.servlet.newuser.NewUserForm;
 
@@ -64,8 +63,16 @@ public class NewUserBean extends PwmSessionBean
     @SerializedName( "u" )
     private boolean urlSpecifiedProfile;
 
-    @SerializedName( "v" )
-    private final TokenVerificationProgress tokenVerificationProgress = new TokenVerificationProgress();
+    @SerializedName( "ct" )
+    private String currentTokenField;
+
+    @SerializedName( "ft" )
+    private Set<String> completedTokenFields = new HashSet<>();
+
+    @SerializedName( "ts" )
+    private boolean tokenSent;
+
+
 
     @Override
     public Type getType( )
@@ -80,8 +87,4 @@ public class NewUserBean extends PwmSessionBean
     }
 
 
-    public TokenVerificationProgress getTokenVerificationProgress( )
-    {
-        return tokenVerificationProgress;
-    }
 }

+ 8 - 8
server/src/main/java/password/pwm/http/bean/UpdateProfileBean.java

@@ -24,7 +24,6 @@ package password.pwm.http.bean;
 
 import com.google.gson.annotations.SerializedName;
 import lombok.Data;
-import password.pwm.bean.TokenVerificationProgress;
 import password.pwm.config.option.SessionBeanMode;
 
 import java.util.Arrays;
@@ -53,8 +52,14 @@ public class UpdateProfileBean extends PwmSessionBean
     @SerializedName( "f" )
     private Map<String, String> formData = new LinkedHashMap<>();
 
-    @SerializedName( "vp" )
-    private TokenVerificationProgress tokenVerificationProgress = new TokenVerificationProgress();
+    @SerializedName( "ct" )
+    private String currentTokenField;
+
+    @SerializedName( "ft" )
+    private Set<String> completedTokenFields = new HashSet<>();
+
+    @SerializedName( "ts" )
+    private boolean tokenSent;
 
     public Type getType( )
     {
@@ -66,9 +71,4 @@ public class UpdateProfileBean extends PwmSessionBean
     {
         return Collections.unmodifiableSet( new HashSet<>( Arrays.asList( SessionBeanMode.LOCAL, SessionBeanMode.CRYPTCOOKIE ) ) );
     }
-
-    public void clearTokenVerificationProgress( )
-    {
-        tokenVerificationProgress = new TokenVerificationProgress();
-    }
 }

+ 2 - 1
server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java

@@ -57,6 +57,7 @@ import password.pwm.http.servlet.newuser.NewUserServlet;
 import password.pwm.http.servlet.oauth.OAuthConsumerServlet;
 import password.pwm.http.servlet.peoplesearch.PrivatePeopleSearchServlet;
 import password.pwm.http.servlet.peoplesearch.PublicPeopleSearchServlet;
+import password.pwm.http.servlet.updateprofile.UpdateProfileServlet;
 
 import javax.servlet.annotation.WebServlet;
 import java.lang.annotation.Annotation;
@@ -75,7 +76,7 @@ public enum PwmServletDefinition
     AccountInformation( AccountInformationServlet.class, null ),
     PrivateChangePassword( PrivateChangePasswordServlet.class, ChangePasswordBean.class ),
     SetupResponses( password.pwm.http.servlet.SetupResponsesServlet.class, SetupResponsesBean.class ),
-    UpdateProfile( password.pwm.http.servlet.UpdateProfileServlet.class, UpdateProfileBean.class ),
+    UpdateProfile( UpdateProfileServlet.class, UpdateProfileBean.class ),
     SetupOtp( password.pwm.http.servlet.SetupOtpServlet.class, SetupOtpBean.class ),
     Helpdesk( password.pwm.http.servlet.helpdesk.HelpdeskServlet.class, null ),
     Shortcuts( password.pwm.http.servlet.ShortcutServlet.class, ShortcutsBean.class ),

+ 0 - 880
server/src/main/java/password/pwm/http/servlet/UpdateProfileServlet.java

@@ -1,880 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.http.servlet;
-
-import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiException;
-import com.novell.ldapchai.exception.ChaiUnavailableException;
-import lombok.Data;
-import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
-import password.pwm.bean.EmailItemBean;
-import password.pwm.bean.SessionLabel;
-import password.pwm.bean.TokenVerificationProgress;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.option.TokenStorageMethod;
-import password.pwm.config.profile.LdapProfile;
-import password.pwm.config.profile.UpdateAttributesProfile;
-import password.pwm.config.value.data.ActionConfiguration;
-import password.pwm.config.value.data.FormConfiguration;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmDataValidationException;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.HttpMethod;
-import password.pwm.http.JspUrl;
-import password.pwm.http.ProcessStatus;
-import password.pwm.http.PwmRequest;
-import password.pwm.http.PwmRequestAttribute;
-import password.pwm.http.PwmSession;
-import password.pwm.http.bean.UpdateProfileBean;
-import password.pwm.i18n.Message;
-import password.pwm.ldap.LdapOperationsHelper;
-import password.pwm.ldap.UserInfo;
-import password.pwm.svc.event.AuditEvent;
-import password.pwm.svc.event.AuditRecord;
-import password.pwm.svc.event.AuditRecordFactory;
-import password.pwm.svc.stats.Statistic;
-import password.pwm.svc.token.TokenPayload;
-import password.pwm.svc.token.TokenService;
-import password.pwm.svc.token.TokenType;
-import password.pwm.util.form.FormUtility;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
-import password.pwm.util.operations.ActionExecutor;
-import password.pwm.ws.server.RestResultBean;
-
-import javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import java.io.IOException;
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * User interaction servlet for updating user attributes.
- *
- * @author Jason D. Rivard
- */
-@WebServlet(
-        name = "UpdateProfileServlet",
-        urlPatterns = {
-                PwmConstants.URL_PREFIX_PRIVATE + "/updateprofile",
-                PwmConstants.URL_PREFIX_PRIVATE + "/UpdateProfile"
-        }
-)
-public class UpdateProfileServlet extends ControlledPwmServlet
-{
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( UpdateProfileServlet.class );
-
-    @Data
-    public static class ValidateResponse implements Serializable
-    {
-        private String message;
-        private boolean success;
-    }
-
-    public enum UpdateProfileAction implements AbstractPwmServlet.ProcessAction
-    {
-        updateProfile( HttpMethod.POST ),
-        agree( HttpMethod.POST ),
-        confirm( HttpMethod.POST ),
-        unConfirm( HttpMethod.POST ),
-        validate( HttpMethod.POST ),
-        enterCode( HttpMethod.POST ),;
-
-        private final HttpMethod method;
-
-        UpdateProfileAction( final HttpMethod method )
-        {
-            this.method = method;
-        }
-
-        public Collection<HttpMethod> permittedMethods( )
-        {
-            return Collections.singletonList( method );
-        }
-    }
-
-    @Override
-    public Class<? extends ProcessAction> getProcessActionsClass( )
-    {
-        return UpdateProfileAction.class;
-    }
-
-    private static UpdateAttributesProfile getProfile( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
-    {
-        return pwmRequest.getPwmSession().getSessionManager().getUpdateAttributeProfile( pwmRequest.getPwmApplication() );
-    }
-
-    private static UpdateProfileBean getBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
-    {
-        return pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, UpdateProfileBean.class );
-    }
-
-    @ActionHandler( action = "enterCode" )
-    ProcessStatus handleEnterCodeRequest(
-            final PwmRequest pwmRequest
-    )
-            throws PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
-
-        boolean tokenPassed = false;
-        ErrorInformation errorInformation = null;
-        try
-        {
-            final TokenPayload tokenPayload = pwmApplication.getTokenService().processUserEnteredCode(
-                    pwmSession,
-                    pwmRequest.getUserInfoIfLoggedIn(),
-                    null,
-                    userEnteredCode
-            );
-            if ( tokenPayload != null )
-            {
-                if ( TokenType.UPDATE_EMAIL.matchesName( tokenPayload.getName() ) )
-                {
-                    LOGGER.debug( pwmRequest, "email token passed" );
-
-                    updateProfileBean.getTokenVerificationProgress().getPassedTokens().add( TokenVerificationProgress.TokenChannel.EMAIL );
-                    updateProfileBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.EMAIL );
-                    updateProfileBean.getTokenVerificationProgress().setPhase( null );
-                    tokenPassed = true;
-                }
-                else if ( TokenType.UPDATE_SMS.matchesName( tokenPayload.getName() ) )
-                {
-                    LOGGER.debug( pwmRequest, "SMS token passed" );
-                    updateProfileBean.getTokenVerificationProgress().getPassedTokens().add( TokenVerificationProgress.TokenChannel.SMS );
-                    updateProfileBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.SMS );
-                    updateProfileBean.getTokenVerificationProgress().setPhase( null );
-                    tokenPassed = true;
-                }
-                else
-                {
-                    final String errorMsg = "token name/type is not recognized: " + tokenPayload.getName();
-                    errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
-                }
-            }
-        }
-        catch ( PwmOperationalException e )
-        {
-            final String errorMsg = "token incorrect: " + e.getMessage();
-            errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
-        }
-
-
-        if ( !tokenPassed )
-        {
-            if ( errorInformation == null )
-            {
-                errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT );
-            }
-            LOGGER.debug( pwmSession, errorInformation.toDebugStr() );
-            setLastError( pwmRequest, errorInformation );
-        }
-
-        return ProcessStatus.Continue;
-    }
-
-    @ActionHandler( action = "validate" )
-    ProcessStatus restValidateForm(
-            final PwmRequest pwmRequest
-    )
-            throws IOException, ServletException, PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
-        final UpdateAttributesProfile updateAttributesProfile = getProfile( pwmRequest );
-
-        boolean success = true;
-        String userMessage = Message.getLocalizedMessage( pwmRequest.getLocale(), Message.Success_UpdateForm, pwmRequest.getConfig() );
-
-        try
-        {
-            // read in the responses from the request
-            final Map<FormConfiguration, String> formValues = readFromJsonRequest( pwmRequest, updateAttributesProfile, updateProfileBean );
-
-            // verify form meets the form requirements
-            verifyFormAttributes( pwmRequest.getPwmApplication(), pwmRequest.getUserInfoIfLoggedIn(), pwmRequest.getLocale(), formValues, true );
-
-            updateProfileBean.getFormData().putAll( FormUtility.asStringMap( formValues ) );
-        }
-        catch ( PwmOperationalException e )
-        {
-            success = false;
-            userMessage = e.getErrorInformation().toUserStr( pwmRequest.getPwmSession(), pwmRequest.getPwmApplication() );
-        }
-
-        final ValidateResponse response = new ValidateResponse();
-        response.setMessage( userMessage );
-        response.setSuccess( success );
-        pwmRequest.outputJsonResult( RestResultBean.withData( response ) );
-        return ProcessStatus.Halt;
-    }
-
-    @ActionHandler( action = "unConfirm" )
-    ProcessStatus handleUnconfirm(
-            final PwmRequest pwmRequest
-    )
-            throws PwmUnrecoverableException
-    {
-        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
-
-        updateProfileBean.setFormSubmitted( false );
-        updateProfileBean.setConfirmationPassed( false );
-        updateProfileBean.clearTokenVerificationProgress();
-
-        return ProcessStatus.Continue;
-    }
-
-    @ActionHandler( action = "agree" )
-    ProcessStatus handleAgreeRequest( final PwmRequest pwmRequest )
-            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException
-    {
-        LOGGER.debug( pwmRequest, "user accepted agreement" );
-
-        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
-        if ( !updateProfileBean.isAgreementPassed() )
-        {
-            updateProfileBean.setAgreementPassed( true );
-            final AuditRecord auditRecord = new AuditRecordFactory( pwmRequest ).createUserAuditRecord(
-                    AuditEvent.AGREEMENT_PASSED,
-                    pwmRequest.getUserInfoIfLoggedIn(),
-                    pwmRequest.getSessionLabel(),
-                    "UpdateProfile"
-            );
-            pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
-        }
-
-        return ProcessStatus.Continue;
-    }
-
-    @ActionHandler( action = "confirm" )
-    ProcessStatus handleConfirmRequest( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException
-    {
-        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
-        updateProfileBean.setConfirmationPassed( true );
-
-        return ProcessStatus.Continue;
-    }
-
-    @ActionHandler( action = "updateProfile" )
-    ProcessStatus handleUpdateProfileRequest(
-            final PwmRequest pwmRequest
-    )
-            throws PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
-        final UpdateAttributesProfile updateAttributesProfile = getProfile( pwmRequest );
-
-        try
-        {
-            readFormParametersFromRequest( pwmRequest, updateAttributesProfile, updateProfileBean );
-        }
-        catch ( PwmOperationalException e )
-        {
-            LOGGER.error( pwmRequest, e.getMessage() );
-            setLastError( pwmRequest, e.getErrorInformation() );
-        }
-
-        updateProfileBean.setFormSubmitted( true );
-
-        return ProcessStatus.Continue;
-    }
-
-    protected void nextStep( final PwmRequest pwmRequest )
-            throws IOException, ServletException, PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
-        final UpdateAttributesProfile updateAttributesProfile = getProfile( pwmRequest );
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-
-        final String updateProfileAgreementText = updateAttributesProfile.readSettingAsLocalizedString(
-                PwmSetting.UPDATE_PROFILE_AGREEMENT_MESSAGE,
-                pwmSession.getSessionStateBean().getLocale()
-        );
-
-        if ( !StringUtil.isEmpty( updateProfileAgreementText ) )
-        {
-            if ( !updateProfileBean.isAgreementPassed() )
-            {
-                final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( pwmRequest.getPwmApplication() );
-                final String expandedText = macroMachine.expandMacros( updateProfileAgreementText );
-                pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
-                pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES_AGREEMENT );
-                return;
-            }
-        }
-
-        //make sure there is form data in the bean.
-        if ( !updateProfileBean.isFormLdapLoaded() )
-        {
-            updateProfileBean.getFormData().clear();
-            updateProfileBean.getFormData().putAll( ( formDataFromLdap( pwmRequest, updateAttributesProfile ) ) );
-            updateProfileBean.setFormLdapLoaded( true );
-            forwardToForm( pwmRequest, updateAttributesProfile, updateProfileBean );
-            return;
-        }
-
-        if ( !updateProfileBean.isFormSubmitted() )
-        {
-            forwardToForm( pwmRequest, updateAttributesProfile, updateProfileBean );
-            return;
-        }
-
-
-        // validate the form data.
-        try
-        {
-            // verify form meets the form requirements
-            final List<FormConfiguration> formFields = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
-            final Map<FormConfiguration, String> formValues = FormUtility.readFormValuesFromMap( updateProfileBean.getFormData(), formFields, pwmRequest.getLocale() );
-            verifyFormAttributes( pwmRequest.getPwmApplication(), pwmRequest.getUserInfoIfLoggedIn(), pwmRequest.getLocale(), formValues, true );
-        }
-        catch ( PwmException e )
-        {
-            LOGGER.error( pwmSession, e.getMessage() );
-            setLastError( pwmRequest, e.getErrorInformation() );
-            forwardToForm( pwmRequest, updateAttributesProfile, updateProfileBean );
-            return;
-        }
-
-        final boolean requireConfirmation = updateAttributesProfile.readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_SHOW_CONFIRMATION );
-        if ( requireConfirmation && !updateProfileBean.isConfirmationPassed() )
-        {
-            forwardToConfirmForm( pwmRequest, updateAttributesProfile, updateProfileBean );
-            return;
-        }
-
-        final Set<TokenVerificationProgress.TokenChannel> requiredVerifications = determineTokenPhaseRequired( pwmRequest, updateProfileBean, updateAttributesProfile );
-        if ( requiredVerifications != null )
-        {
-            for ( final TokenVerificationProgress.TokenChannel tokenChannel : requiredVerifications )
-            {
-                if ( requiredVerifications.contains( tokenChannel ) )
-                {
-                    if ( !updateProfileBean.getTokenVerificationProgress().getIssuedTokens().contains( tokenChannel ) )
-                    {
-                        initializeToken( pwmRequest, updateProfileBean, tokenChannel );
-                    }
-
-                    if ( !updateProfileBean.getTokenVerificationProgress().getPassedTokens().contains( tokenChannel ) )
-                    {
-                        updateProfileBean.getTokenVerificationProgress().setPhase( tokenChannel );
-                        pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES_ENTER_CODE );
-                        return;
-                    }
-                }
-            }
-        }
-
-        try
-        {
-            // write the form values
-            final ChaiUser theUser = pwmSession.getSessionManager().getActor( pwmApplication );
-            doProfileUpdate(
-                    pwmRequest.getPwmApplication(),
-                    pwmRequest.getSessionLabel(),
-                    pwmRequest.getLocale(),
-                    pwmSession.getUserInfo(),
-                    pwmSession.getSessionManager().getMacroMachine( pwmApplication ),
-                    updateAttributesProfile,
-                    updateProfileBean.getFormData(),
-                    theUser
-            );
-
-            // re-populate the uiBean because we have changed some values.
-            pwmSession.reloadUserInfoBean( pwmApplication );
-
-            // clear cached read attributes.
-            pwmRequest.getPwmSession().reloadUserInfoBean( pwmApplication );
-
-            // mark the event log
-            pwmApplication.getAuditManager().submit( AuditEvent.UPDATE_PROFILE, pwmSession.getUserInfo(), pwmSession );
-
-            // clear the bean
-            pwmApplication.getSessionStateService().clearBean( pwmRequest, UpdateProfileBean.class );
-
-            pwmRequest.getPwmResponse().forwardToSuccessPage( Message.Success_UpdateProfile );
-            return;
-        }
-        catch ( PwmException e )
-        {
-            LOGGER.error( pwmSession, e.getMessage() );
-            setLastError( pwmRequest, e.getErrorInformation() );
-        }
-        catch ( ChaiException e )
-        {
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UPDATE_ATTRS_FAILURE, e.toString() );
-            LOGGER.error( pwmSession, errorInformation.toDebugStr() );
-            setLastError( pwmRequest, errorInformation );
-        }
-
-        forwardToForm( pwmRequest, updateAttributesProfile, updateProfileBean );
-    }
-
-    @Override
-    public ProcessStatus preProcessCheck( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException
-    {
-        if ( !pwmRequest.getPwmApplication().getConfig().readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_ENABLE ) )
-        {
-            pwmRequest.respondWithError( new ErrorInformation(
-                    PwmError.ERROR_SERVICE_NOT_AVAILABLE,
-                    "Setting " + PwmSetting.UPDATE_PROFILE_ENABLE.toMenuLocationDebug( null, null ) + " is not enabled." )
-            );
-            return ProcessStatus.Halt;
-        }
-
-        final UpdateAttributesProfile updateAttributesProfile = getProfile( pwmRequest );
-        if ( updateAttributesProfile == null )
-        {
-            pwmRequest.respondWithError( new ErrorInformation( PwmError.ERROR_NO_PROFILE_ASSIGNED ) );
-            return ProcessStatus.Halt;
-        }
-
-        return ProcessStatus.Continue;
-    }
-
-
-    final Map<FormConfiguration, String> readFormParametersFromRequest(
-            final PwmRequest pwmRequest,
-            final UpdateAttributesProfile updateAttributesProfile,
-            final UpdateProfileBean updateProfileBean
-    )
-            throws PwmUnrecoverableException, PwmDataValidationException, ChaiUnavailableException
-    {
-        final List<FormConfiguration> formFields = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
-
-        //read the values from the request
-        final Map<FormConfiguration, String> formValueMap = FormUtility.readFormValuesFromRequest( pwmRequest, formFields, pwmRequest.getLocale() );
-
-        return updateBeanFormData( formFields, formValueMap, updateProfileBean );
-    }
-
-    static Map<FormConfiguration, String> readFromJsonRequest(
-            final PwmRequest pwmRequest,
-            final UpdateAttributesProfile updateAttributesProfile,
-            final UpdateProfileBean updateProfileBean
-    )
-            throws PwmDataValidationException, PwmUnrecoverableException, IOException
-    {
-        final List<FormConfiguration> formFields = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
-
-        final Map<FormConfiguration, String> formValueMap = FormUtility.readFormValuesFromMap( pwmRequest.readBodyAsJsonStringMap(), formFields, pwmRequest.getLocale() );
-
-        return updateBeanFormData( formFields, formValueMap, updateProfileBean );
-    }
-
-    static Map<FormConfiguration, String> updateBeanFormData(
-            final List<FormConfiguration> formFields,
-            final Map<FormConfiguration, String> formValueMap,
-            final UpdateProfileBean updateProfileBean
-    )
-    {
-        final LinkedHashMap<FormConfiguration, String> newFormValueMap = new LinkedHashMap<>();
-        for ( final FormConfiguration formConfiguration : formFields )
-        {
-            if ( formConfiguration.isReadonly() )
-            {
-                final String existingValue = updateProfileBean.getFormData().get( formConfiguration.getName() );
-                newFormValueMap.put( formConfiguration, existingValue );
-            }
-            else
-            {
-                newFormValueMap.put( formConfiguration, formValueMap.get( formConfiguration ) );
-            }
-        }
-
-        updateProfileBean.getFormData().clear();
-        updateProfileBean.getFormData().putAll( FormUtility.asStringMap( newFormValueMap ) );
-
-        return newFormValueMap;
-    }
-
-
-    @SuppressWarnings( "checkstyle:ParameterNumber" )
-    public static void doProfileUpdate(
-            final PwmApplication pwmApplication,
-            final SessionLabel sessionLabel,
-            final Locale locale,
-            final UserInfo userInfo,
-            final MacroMachine macroMachine,
-            final UpdateAttributesProfile updateAttributesProfile,
-            final Map<String, String> formValues,
-            final ChaiUser theUser
-    )
-            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
-    {
-        final List<FormConfiguration> formFields = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
-        final Map<FormConfiguration, String> formMap = FormUtility.readFormValuesFromMap( formValues, formFields, locale );
-
-        // verify form meets the form requirements (may be redundant, but shouldn't hurt)
-        verifyFormAttributes( pwmApplication, userInfo.getUserIdentity(), locale, formMap, false );
-
-        // write values.
-        LOGGER.info( "updating profile for " + userInfo.getUserIdentity() );
-
-        LdapOperationsHelper.writeFormValuesToLdap( pwmApplication, macroMachine, theUser, formMap, false );
-
-        final UserIdentity userIdentity = userInfo.getUserIdentity();
-
-        {
-            // execute configured actions
-            final List<ActionConfiguration> actions = updateAttributesProfile.readSettingAsAction( PwmSetting.UPDATE_PROFILE_WRITE_ATTRIBUTES );
-            if ( actions != null && !actions.isEmpty() )
-            {
-                LOGGER.debug( sessionLabel, "executing configured actions to user " + userIdentity );
-
-
-                final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
-                        .setExpandPwmMacros( true )
-                        .setMacroMachine( macroMachine )
-                        .createActionExecutor();
-
-                actionExecutor.executeActions( actions, sessionLabel );
-            }
-        }
-        sendProfileUpdateEmailNotice( pwmApplication, macroMachine, userInfo, locale, sessionLabel );
-
-        // success, so forward to success page
-        pwmApplication.getStatisticsManager().incrementValue( Statistic.UPDATE_ATTRIBUTES );
-    }
-
-    private static void verifyFormAttributes(
-            final PwmApplication pwmApplication,
-            final UserIdentity userIdentity,
-            final Locale userLocale,
-            final Map<FormConfiguration, String> formValues,
-            final boolean allowResultCaching
-    )
-            throws PwmOperationalException, PwmUnrecoverableException
-    {
-        // see if the values meet form requirements.
-        FormUtility.validateFormValues( pwmApplication.getConfig(), formValues, userLocale );
-
-        final List<FormUtility.ValidationFlag> validationFlags = new ArrayList<>();
-        if ( allowResultCaching )
-        {
-            validationFlags.add( FormUtility.ValidationFlag.allowResultCaching );
-        }
-
-        // check unique fields against ldap
-        FormUtility.validateFormValueUniqueness(
-                pwmApplication,
-                formValues,
-                userLocale,
-                Collections.singletonList( userIdentity ),
-                validationFlags.toArray( new FormUtility.ValidationFlag[ validationFlags.size() ] )
-        );
-    }
-
-    private static void sendProfileUpdateEmailNotice(
-            final PwmApplication pwmApplication,
-            final MacroMachine macroMachine,
-            final UserInfo userInfo,
-            final Locale locale,
-            final SessionLabel sessionLabel
-    )
-            throws PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final Configuration config = pwmApplication.getConfig();
-
-        final EmailItemBean configuredEmailSetting = config.readSettingAsEmail( PwmSetting.EMAIL_UPDATEPROFILE, locale );
-        pwmApplication.getEmailQueue().submitEmail(
-                configuredEmailSetting,
-                userInfo,
-                macroMachine
-        );
-
-        if ( configuredEmailSetting == null )
-        {
-            LOGGER.debug( sessionLabel, "skipping send profile update email for '" + userInfo.getUserIdentity().toDisplayString() + "' no email configured" );
-        }
-    }
-
-    private static void forwardToForm( final PwmRequest pwmRequest, final UpdateAttributesProfile updateAttributesProfile, final UpdateProfileBean updateProfileBean )
-            throws ServletException, PwmUnrecoverableException, IOException
-    {
-        final List<FormConfiguration> form = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
-        final Map<FormConfiguration, String> formValueMap = formMapFromBean( updateAttributesProfile, updateProfileBean );
-        pwmRequest.addFormInfoToRequestAttr( form, formValueMap, false, false );
-        final List<FormConfiguration> links = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_CUSTOMLINKS );
-        pwmRequest.setAttribute( PwmRequestAttribute.FormCustomLinks, new ArrayList<>( links ) );
-        pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES );
-    }
-
-    private static void forwardToConfirmForm( final PwmRequest pwmRequest, final UpdateAttributesProfile updateAttributesProfile, final UpdateProfileBean updateProfileBean )
-            throws ServletException, PwmUnrecoverableException, IOException
-    {
-        final List<FormConfiguration> form = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
-        final Map<FormConfiguration, String> formValueMap = formMapFromBean( updateAttributesProfile, updateProfileBean );
-        pwmRequest.addFormInfoToRequestAttr( form, formValueMap, true, false );
-        pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES_CONFIRM );
-    }
-
-    private static Map<FormConfiguration, String> formMapFromBean(
-            final UpdateAttributesProfile updateAttributesProfile,
-            final UpdateProfileBean updateProfileBean
-    )
-            throws PwmUnrecoverableException
-    {
-
-        final List<FormConfiguration> form = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
-        final Map<FormConfiguration, String> formValueMap = new LinkedHashMap<>();
-        for ( final FormConfiguration formConfiguration : form )
-        {
-            formValueMap.put(
-                    formConfiguration,
-                    updateProfileBean.getFormData().keySet().contains( formConfiguration.getName() )
-                            ? updateProfileBean.getFormData().get( formConfiguration.getName() )
-                            : ""
-            );
-        }
-        return formValueMap;
-    }
-
-    private static Map<String, String> formDataFromLdap( final PwmRequest pwmRequest, final UpdateAttributesProfile updateAttributesProfile )
-            throws PwmUnrecoverableException
-    {
-        final UserInfo userInfo = pwmRequest.getPwmSession().getUserInfo();
-        final List<FormConfiguration> formFields = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
-        final Map<FormConfiguration, String> formMap = new LinkedHashMap<>();
-        FormUtility.populateFormMapFromLdap( formFields, pwmRequest.getSessionLabel(), formMap, userInfo );
-        return FormUtility.asStringMap( formMap );
-    }
-
-    private static Set<TokenVerificationProgress.TokenChannel> determineTokenPhaseRequired(
-            final PwmRequest pwmRequest,
-            final UpdateProfileBean updateProfileBean,
-            final UpdateAttributesProfile updateAttributesProfile
-
-    )
-            throws PwmUnrecoverableException
-    {
-        final Set<TokenVerificationProgress.TokenChannel> returnObj = new HashSet<>();
-
-        final LdapProfile ldapProfile = pwmRequest.getUserInfoIfLoggedIn().getLdapProfile( pwmRequest.getConfig() );
-        final Map<String, String> userFormData = updateProfileBean.getFormData();
-        Map<String, String> ldapData = null;
-
-        if ( updateAttributesProfile.readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_EMAIL_VERIFICATION ) )
-        {
-            final String emailAddressAttribute = ldapProfile.readSettingAsString( PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE );
-            if ( userFormData.containsKey( emailAddressAttribute ) && !userFormData.get( emailAddressAttribute ).isEmpty() )
-            {
-                ldapData = formDataFromLdap( pwmRequest, updateAttributesProfile );
-                if ( userFormData.get( emailAddressAttribute ) != null && !userFormData.get( emailAddressAttribute ).equalsIgnoreCase( ldapData.get( emailAddressAttribute ) ) )
-                {
-                    returnObj.add( TokenVerificationProgress.TokenChannel.EMAIL );
-                }
-            }
-            else
-            {
-                LOGGER.warn( pwmRequest, "email verification enabled, but email attribute '" + emailAddressAttribute + "' is not in update form" );
-            }
-        }
-
-        if ( updateAttributesProfile.readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_SMS_VERIFICATION ) )
-        {
-            final String phoneNumberAttribute = ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE );
-            if ( userFormData.containsKey( phoneNumberAttribute ) && !userFormData.get( phoneNumberAttribute ).isEmpty() )
-            {
-                if ( ldapData == null )
-                {
-                    ldapData = formDataFromLdap( pwmRequest, updateAttributesProfile );
-                }
-                if ( userFormData.get( phoneNumberAttribute ) != null && !userFormData.get( phoneNumberAttribute ).equalsIgnoreCase( ldapData.get( phoneNumberAttribute ) ) )
-                {
-                    returnObj.add( TokenVerificationProgress.TokenChannel.SMS );
-                }
-            }
-            else
-            {
-                LOGGER.warn( pwmRequest, "sms verification enabled, but phone attribute '" + phoneNumberAttribute + "' is not in update form" );
-            }
-        }
-
-        LOGGER.trace( pwmRequest, "determined required verification phases: " + StringUtil.collectionToString( returnObj, "," ) );
-
-        return returnObj;
-    }
-
-    private void initializeToken(
-            final PwmRequest pwmRequest,
-            final UpdateProfileBean updateProfileBean,
-            final TokenVerificationProgress.TokenChannel tokenType
-    )
-            throws PwmUnrecoverableException
-    {
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-
-        if ( pwmApplication.getConfig().getTokenStorageMethod() == TokenStorageMethod.STORE_LDAP )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
-                    "cannot generate new user tokens when storage type is configured as STORE_LDAP.",
-            } ) );
-        }
-
-        final UpdateAttributesProfile profile = getProfile( pwmRequest );
-        final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( pwmApplication );
-        final Configuration config = pwmApplication.getConfig();
-        final LdapProfile ldapProfile = pwmRequest.getUserInfoIfLoggedIn().getLdapProfile( pwmRequest.getConfig() );
-
-        switch ( tokenType )
-        {
-            case SMS:
-            {
-                final String telephoneNumberAttribute = ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE );
-                final String toNum = updateProfileBean.getFormData().get( telephoneNumberAttribute );
-                final String tokenKey;
-                try
-                {
-                    final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
-                            TokenType.UPDATE_SMS,
-                            profile.getTokenDurationSMS( config ),
-                            Collections.emptyMap(),
-                            pwmRequest.getUserInfoIfLoggedIn(),
-                            Collections.singleton( toNum )
-                    );
-                    tokenKey = pwmApplication.getTokenService().generateNewToken( tokenPayload,
-                            pwmRequest.getSessionLabel() );
-                }
-                catch ( PwmOperationalException e )
-                {
-                    throw new PwmUnrecoverableException( e.getErrorInformation() );
-                }
-
-                final String message = config.readSettingAsLocalizedString( PwmSetting.SMS_UPDATE_PROFILE_TOKEN_TEXT,
-                        pwmSession.getSessionStateBean().getLocale() );
-
-                try
-                {
-                    TokenService.TokenSender.sendSmsToken(
-                            TokenService.TokenSendInfo.builder()
-                                    .pwmApplication( pwmApplication )
-                                    .userInfo( null )
-                                    .macroMachine( macroMachine )
-                                    .smsNumber( toNum )
-                                    .smsMessage( message )
-                                    .tokenKey( tokenKey )
-                                    .sessionLabel( pwmSession.getLabel() )
-                                    .build()
-                    );
-                }
-                catch ( Exception e )
-                {
-                    throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_UNKNOWN ) );
-                }
-
-                updateProfileBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.SMS );
-                updateProfileBean.getTokenVerificationProgress().setTokenDisplayText( toNum );
-                updateProfileBean.getTokenVerificationProgress().setPhase( TokenVerificationProgress.TokenChannel.SMS );
-            }
-            break;
-
-            case EMAIL:
-            {
-                final EmailItemBean configuredEmailSetting = config.readSettingAsEmail(
-                        PwmSetting.EMAIL_UPDATEPROFILE_VERIFICATION,
-                        pwmRequest.getLocale()
-                );
-                final String emailAddressAttribute = ldapProfile.readSettingAsString( PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE );
-                final String toAddress = updateProfileBean.getFormData().get( emailAddressAttribute );
-
-                final String tokenKey;
-                try
-                {
-                    final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
-                            TokenType.UPDATE_EMAIL,
-                            profile.getTokenDurationEmail( config ),
-                            Collections.emptyMap(),
-                            pwmRequest.getUserInfoIfLoggedIn(),
-                            Collections.singleton( toAddress )
-                    );
-                    tokenKey = pwmApplication.getTokenService().generateNewToken( tokenPayload,
-                            pwmRequest.getSessionLabel() );
-                }
-                catch ( PwmOperationalException e )
-                {
-                    throw new PwmUnrecoverableException( e.getErrorInformation() );
-                }
-
-                updateProfileBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.EMAIL );
-                updateProfileBean.getTokenVerificationProgress().setPhase( TokenVerificationProgress.TokenChannel.EMAIL );
-                updateProfileBean.getTokenVerificationProgress().setTokenDisplayText( toAddress );
-
-                final EmailItemBean emailItemBean = new EmailItemBean(
-                        toAddress,
-                        configuredEmailSetting.getFrom(),
-                        configuredEmailSetting.getSubject(),
-                        configuredEmailSetting.getBodyPlain().replace( "%TOKEN%", tokenKey ),
-                        configuredEmailSetting.getBodyHtml().replace( "%TOKEN%", tokenKey ) );
-
-                try
-                {
-                    TokenService.TokenSender.sendEmailToken(
-                            TokenService.TokenSendInfo.builder()
-                                    .pwmApplication( pwmApplication )
-                                    .userInfo( null )
-                                    .macroMachine( macroMachine )
-                                    .configuredEmailSetting( emailItemBean )
-                                    .emailAddress( toAddress )
-                                    .tokenKey( tokenKey )
-                                    .sessionLabel( pwmRequest.getSessionLabel() )
-                                    .build()
-                    );
-                }
-                catch ( Exception e )
-                {
-                    throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_UNKNOWN ) );
-                }
-            }
-            break;
-
-            default:
-                LOGGER.error( "Unimplemented token purpose: " + tokenType );
-                updateProfileBean.getTokenVerificationProgress().setPhase( null );
-        }
-    }
-
-
-}
-

+ 16 - 13
server/src/main/java/password/pwm/http/servlet/activation/ActivateUserServlet.java

@@ -57,6 +57,7 @@ import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.token.TokenPayload;
+import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.CaptchaUtility;
@@ -328,28 +329,30 @@ public class ActivateUserServlet extends ControlledPwmServlet
         final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
         final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
 
+
         ErrorInformation errorInformation = null;
         try
         {
-            final TokenPayload tokenPayload = pwmApplication.getTokenService().processUserEnteredCode(
-                    pwmSession,
-                    activateUserBean.getUserIdentity(),
+            final TokenPayload tokenPayload = TokenUtil.checkEnteredCode(
+                    pwmRequest,
+                    userEnteredCode,
+                    activateUserBean.getTokenDestination(),
+                    null,
                     TokenType.ACTIVATION,
-                    userEnteredCode
+                    TokenService.TokenEntryType.unauthenticated
             );
-            if ( tokenPayload != null )
-            {
-                activateUserBean.setUserIdentity( tokenPayload.getUserIdentity() );
-                activateUserBean.setTokenPassed( true );
-                activateUserBean.setFormValidated( true );
-            }
+
+            activateUserBean.setUserIdentity( tokenPayload.getUserIdentity() );
+            activateUserBean.setTokenPassed( true );
+            activateUserBean.setFormValidated( true );
         }
-        catch ( PwmOperationalException e )
+        catch ( PwmUnrecoverableException e )
         {
-            final String errorMsg = "token incorrect: " + e.getMessage();
-            errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
+            LOGGER.debug( pwmRequest, "error while checking entered token: " );
+            errorInformation = e.getErrorInformation();
         }
 
+
         if ( !activateUserBean.isTokenPassed() )
         {
             if ( errorInformation == null )

+ 26 - 17
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -76,7 +76,9 @@ import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.token.TokenPayload;
+import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenType;
+import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.CaptchaUtility;
 import password.pwm.util.PostChangePasswordAction;
 import password.pwm.util.form.FormUtility;
@@ -506,30 +508,37 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
         final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
 
+
+
         ErrorInformation errorInformation = null;
         try
         {
-            final TokenPayload tokenPayload = pwmRequest.getPwmApplication().getTokenService().processUserEnteredCode(
-                    pwmRequest.getPwmSession(),
-                    forgottenPasswordBean.getUserIdentity() == null ? null : forgottenPasswordBean.getUserIdentity(),
+            final TokenPayload tokenPayload = TokenUtil.checkEnteredCode(
+                    pwmRequest,
+                    userEnteredCode,
+                    forgottenPasswordBean.getProgress().getTokenDestination(),
+                    null,
                     TokenType.FORGOTTEN_PW,
-                    userEnteredCode
+                    TokenService.TokenEntryType.unauthenticated
             );
-            if ( tokenPayload != null )
+
+            // token correct
+            if ( forgottenPasswordBean.getUserIdentity() == null )
             {
-                // token correct
-                if ( forgottenPasswordBean.getUserIdentity() == null )
-                {
-                    // clean session, user supplied token (clicked email, etc) and this is first request
-                    ForgottenPasswordUtil.initForgottenPasswordBean(
-                            pwmRequest,
-                            tokenPayload.getUserIdentity(),
-                            forgottenPasswordBean
-                    );
-                }
-                forgottenPasswordBean.getProgress().getSatisfiedMethods().add( IdentityVerificationMethod.TOKEN );
-                StatisticsManager.incrementStat( pwmRequest.getPwmApplication(), Statistic.RECOVERY_TOKENS_PASSED );
+                // clean session, user supplied token (clicked email, etc) and this is first request
+                ForgottenPasswordUtil.initForgottenPasswordBean(
+                        pwmRequest,
+                        tokenPayload.getUserIdentity(),
+                        forgottenPasswordBean
+                );
             }
+            forgottenPasswordBean.getProgress().getSatisfiedMethods().add( IdentityVerificationMethod.TOKEN );
+            StatisticsManager.incrementStat( pwmRequest.getPwmApplication(), Statistic.RECOVERY_TOKENS_PASSED );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.debug( pwmRequest, "error while checking entered token: " );
+            errorInformation = e.getErrorInformation();
         }
         catch ( PwmOperationalException e )
         {

+ 64 - 80
server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java

@@ -25,7 +25,7 @@ package password.pwm.http.servlet.newuser;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
-import password.pwm.bean.TokenVerificationProgress;
+import password.pwm.bean.TokenDestinationItem;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.NewUserProfile;
@@ -51,6 +51,9 @@ import password.pwm.i18n.Message;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoBean;
 import password.pwm.svc.token.TokenPayload;
+import password.pwm.svc.token.TokenService;
+import password.pwm.svc.token.TokenType;
+import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.CaptchaUtility;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JsonUtil;
@@ -255,41 +258,11 @@ public class NewUserServlet extends ControlledPwmServlet
             }
         }
 
-        final TokenVerificationProgress tokenVerificationProgress = newUserBean.getTokenVerificationProgress();
-        if ( newUserProfile.readSettingAsBoolean( PwmSetting.NEWUSER_EMAIL_VERIFICATION ) )
+        if ( NewUserUtils.checkForTokenVerificationProgress( pwmRequest, newUserBean, newUserProfile ) == ProcessStatus.Halt )
         {
-            if ( !tokenVerificationProgress.getIssuedTokens().contains( TokenVerificationProgress.TokenChannel.EMAIL ) )
-            {
-                NewUserUtils.initializeToken( pwmRequest, newUserBean, TokenVerificationProgress.TokenChannel.EMAIL );
-            }
-
-            if ( !tokenVerificationProgress.getPassedTokens().contains( TokenVerificationProgress.TokenChannel.EMAIL )
-                    //if the token has been sent during the InitializeToken call, the issuedTokens member must contains the SMS key.
-                    // If not, the token has not been sent (SMS number is null) and the verification phase should be ignored
-                    && tokenVerificationProgress.getIssuedTokens().contains( TokenVerificationProgress.TokenChannel.EMAIL )
-                    )
-            {
-                pwmRequest.forwardToJsp( JspUrl.NEW_USER_ENTER_CODE );
-                return;
-            }
+            return;
         }
 
-        if ( newUserProfile.readSettingAsBoolean( PwmSetting.NEWUSER_SMS_VERIFICATION ) )
-        {
-            if ( !newUserBean.getTokenVerificationProgress().getIssuedTokens().contains( TokenVerificationProgress.TokenChannel.SMS ) )
-            {
-                NewUserUtils.initializeToken( pwmRequest, newUserBean, TokenVerificationProgress.TokenChannel.SMS );
-            }
-            if ( !newUserBean.getTokenVerificationProgress().getPassedTokens().contains( TokenVerificationProgress.TokenChannel.SMS )
-                    // if the token has been sent during the InitializeToken call, the issuedTokens member must contains the SMS key.
-                    // If not, the token has not been sent (SMS number is null) and the verification phase should be ignored
-                    && newUserBean.getTokenVerificationProgress().getIssuedTokens().contains( TokenVerificationProgress.TokenChannel.SMS )
-                    )
-            {
-                pwmRequest.forwardToJsp( JspUrl.NEW_USER_ENTER_CODE );
-                return;
-            }
-        }
 
         final String newUserAgreementText = newUserProfile.readSettingAsLocalizedString( PwmSetting.NEWUSER_AGREEMENT_MESSAGE,
                 pwmSession.getSessionStateBean().getLocale() );
@@ -300,7 +273,8 @@ public class NewUserServlet extends ControlledPwmServlet
                 final MacroMachine macroMachine = NewUserUtils.createMacroMachineForNewUser(
                         pwmApplication,
                         pwmRequest.getSessionLabel(),
-                        newUserBean.getNewUserForm()
+                        newUserBean.getNewUserForm(),
+                        null
                 );
                 final String expandedText = macroMachine.expandMacros( newUserAgreementText );
                 pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
@@ -475,89 +449,88 @@ public class NewUserServlet extends ControlledPwmServlet
     private ProcessStatus handleEnterCodeRequest( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException
     {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final NewUserBean newUserBean = getNewUserBean( pwmRequest );
+        final NewUserProfile newUserProfile = getNewUserProfile( pwmRequest );
         final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
 
-        boolean tokenPassed = false;
+        final TokenDestinationItem tokenDestinationItem = NewUserUtils.tokenDestinationItemForCurrentValidation(
+                pwmRequest,
+                newUserBean,
+                newUserProfile
+        );
+
         ErrorInformation errorInformation = null;
+        TokenPayload tokenPayload = null;
         try
         {
-            final TokenPayload tokenPayload = pwmApplication.getTokenService().processUserEnteredCode(
-                    pwmSession,
-                    null,
+            tokenPayload = TokenUtil.checkEnteredCode(
+                    pwmRequest,
+                    userEnteredCode,
+                    tokenDestinationItem,
                     null,
-                    userEnteredCode
+                    TokenType.NEWUSER,
+                    TokenService.TokenEntryType.unauthenticated
             );
-            if ( tokenPayload != null )
+
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.debug( pwmRequest, "error while checking entered token: " );
+            errorInformation = e.getErrorInformation();
+        }
+
+        if ( tokenPayload != null )
+        {
+            try
             {
-                final NewUserBean newUserBean = getNewUserBean( pwmRequest );
                 final NewUserTokenData newUserTokenData = NewUserFormUtils.fromTokenPayload( pwmRequest, tokenPayload );
                 newUserBean.setProfileID( newUserTokenData.getProfileID() );
                 final NewUserForm newUserFormFromToken = newUserTokenData.getFormData();
-                if ( password.pwm.svc.token.TokenType.NEWUSER_EMAIL.matchesName( tokenPayload.getName() ) )
+
+                if ( tokenDestinationItem.getType() == TokenDestinationItem.Type.email )
                 {
-                    LOGGER.debug( pwmRequest, "email token passed" );
 
                     try
                     {
                         verifyForm( pwmRequest, newUserFormFromToken, false );
+                        newUserBean.setRemoteInputData( newUserTokenData.getInjectionData() );
+                        newUserBean.setNewUserForm( newUserFormFromToken );
+                        newUserBean.setFormPassed( true );
                     }
                     catch ( PwmUnrecoverableException | PwmOperationalException e )
                     {
                         LOGGER.error( pwmRequest, "while reading stored form data in token payload, form validation error occurred: " + e.getMessage() );
-                        throw e;
+                        errorInformation = e.getErrorInformation();
                     }
-
-                    newUserBean.setRemoteInputData( newUserTokenData.getInjectionData() );
-                    newUserBean.setNewUserForm( newUserFormFromToken );
-                    newUserBean.setFormPassed( true );
-                    newUserBean.getTokenVerificationProgress().getPassedTokens().add( TokenVerificationProgress.TokenChannel.EMAIL );
-                    newUserBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.EMAIL );
-                    newUserBean.getTokenVerificationProgress().setPhase( null );
-                    tokenPassed = true;
                 }
-                else if ( password.pwm.svc.token.TokenType.NEWUSER_SMS.matchesName( tokenPayload.getName() ) )
+                else if ( tokenDestinationItem.getType() == TokenDestinationItem.Type.sms )
                 {
-                    if ( newUserBean.getNewUserForm() != null && newUserBean.getNewUserForm().isConsistentWith( newUserFormFromToken ) )
+                    if ( newUserBean.getNewUserForm() == null || newUserBean.getNewUserForm().isConsistentWith( newUserFormFromToken ) )
                     {
-                        LOGGER.debug( pwmRequest, "SMS token passed" );
-                        newUserBean.getTokenVerificationProgress().getPassedTokens().add( TokenVerificationProgress.TokenChannel.SMS );
-                        newUserBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.SMS );
-                        newUserBean.getTokenVerificationProgress().setPhase( null );
-                        tokenPassed = true;
-                    }
-                    else
-                    {
-                        LOGGER.debug( pwmRequest, "SMS token value is valid, but form data does not match current session form data" );
+                        LOGGER.debug( pwmRequest, "token value is valid, but form data does not match current session form data" );
                         final String errorMsg = "sms token does not match current session";
                         errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
                     }
                 }
-                else
-                {
-                    final String errorMsg = "token name/type is not recognized: " + tokenPayload.getName();
-                    errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
-                }
+            }
+            catch ( PwmOperationalException e )
+            {
+                errorInformation = e.getErrorInformation();
             }
         }
-        catch ( PwmOperationalException e )
-        {
-            final String errorMsg = "token incorrect: " + e.getMessage();
-            errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
-        }
-
 
-        if ( !tokenPassed )
+        if ( errorInformation != null )
         {
-            if ( errorInformation == null )
-            {
-                errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT );
-            }
             LOGGER.debug( pwmSession, errorInformation.toDebugStr() );
             setLastError( pwmRequest, errorInformation );
+            return ProcessStatus.Continue;
         }
 
+        LOGGER.debug( pwmRequest, "marking token as passed " + JsonUtil.serialize( tokenDestinationItem ) );
+        newUserBean.getCompletedTokenFields().add( newUserBean.getCurrentTokenField() );
+        newUserBean.setTokenSent( false );
+        newUserBean.setCurrentTokenField( null );
         return ProcessStatus.Continue;
     }
 
@@ -770,6 +743,17 @@ public class NewUserServlet extends ControlledPwmServlet
     }
 
 
+    static void forwardToEnterCode( final PwmRequest pwmRequest, final NewUserProfile newUserProfile, final NewUserBean newUserBean )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        final TokenDestinationItem tokenDestinationItem = NewUserUtils.tokenDestinationItemForCurrentValidation(
+                pwmRequest,
+                newUserBean,
+                newUserProfile );
+        pwmRequest.setAttribute( PwmRequestAttribute.TokenDestItems, tokenDestinationItem );
+        pwmRequest.forwardToJsp( JspUrl.NEW_USER_ENTER_CODE );
+    }
+
     private void forwardToFormPage( final PwmRequest pwmRequest, final NewUserBean newUserBean )
             throws ServletException, PwmUnrecoverableException, IOException
     {

+ 155 - 184
server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java

@@ -34,7 +34,7 @@ import password.pwm.PwmApplication;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.SessionLabel;
-import password.pwm.bean.TokenVerificationProgress;
+import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
@@ -43,11 +43,13 @@ import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.value.data.ActionConfiguration;
+import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.ProcessStatus;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.http.bean.NewUserBean;
@@ -59,11 +61,11 @@ import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.stats.Statistic;
-import password.pwm.svc.token.TokenPayload;
-import password.pwm.svc.token.TokenService;
+import password.pwm.svc.token.TokenType;
+import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.PasswordData;
 import password.pwm.util.RandomPasswordGenerator;
-import password.pwm.svc.token.TokenDestinationDisplayMasker;
+import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
@@ -72,13 +74,16 @@ import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.PasswordUtility;
-import password.pwm.ws.client.rest.RestTokenDataClient;
 import password.pwm.ws.client.rest.form.FormDataRequestBean;
 import password.pwm.ws.client.rest.form.FormDataResponseBean;
 import password.pwm.ws.client.rest.form.RestFormDataClient;
 
+import javax.servlet.ServletException;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -360,7 +365,7 @@ class NewUserUtils
     )
             throws PwmUnrecoverableException, ChaiUnavailableException
     {
-        final MacroMachine macroMachine = createMacroMachineForNewUser( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), formValues );
+        final MacroMachine macroMachine = createMacroMachineForNewUser( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), formValues, null );
         final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile( pwmRequest );
         final List<String> configuredNames = newUserProfile.readSettingAsStringArray( PwmSetting.NEWUSER_USERNAME_DEFINITION );
         final List<String> failedValues = new ArrayList<>();
@@ -480,7 +485,8 @@ class NewUserUtils
     static MacroMachine createMacroMachineForNewUser(
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
-            final NewUserForm newUserForm
+            final NewUserForm newUserForm,
+            final TokenDestinationItem tokenDestinationItem
     )
             throws PwmUnrecoverableException
     {
@@ -500,184 +506,11 @@ class NewUserUtils
                 .attributes( formValues )
                 .build();
 
-        return MacroMachine.forUser( pwmApplication, sessionLabel, stubUserBean, stubLoginBean );
-    }
-
-    @SuppressWarnings( "checkstyle:MethodLength" )
-    static void initializeToken(
-            final PwmRequest pwmRequest,
-            final NewUserBean newUserBean,
-            final TokenVerificationProgress.TokenChannel tokenType
-    )
-            throws PwmUnrecoverableException
-    {
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-
-        if ( pwmApplication.getConfig().getTokenStorageMethod() == TokenStorageMethod.STORE_LDAP )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
-                    "cannot generate new user tokens when storage type is configured as STORE_LDAP.",
-            } ) );
-        }
-
-        final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile( pwmRequest );
-        final Configuration config = pwmApplication.getConfig();
-        final Map<String, String> tokenPayloadMap = NewUserFormUtils.toTokenPayload( pwmRequest, newUserBean );
-        final MacroMachine macroMachine = createMacroMachineForNewUser( pwmApplication, pwmRequest.getSessionLabel(), newUserBean.getNewUserForm() );
-
-        switch ( tokenType )
-        {
-            case SMS:
-            {
-                String toNum = null;
-                final NewUserForm userForm = newUserBean.getNewUserForm();
-                if ( userForm != null
-                        && userForm.getFormData() != null
-                        && userForm.getFormData().get( pwmApplication.getConfig().getDefaultLdapProfile().readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE ) ) != null
-                        )
-                {
-                    toNum = userForm.getFormData().get( pwmApplication.getConfig().getDefaultLdapProfile().readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE ) );
-                    if ( toNum.isEmpty() )
-                    {
-                        toNum = null;
-                    }
-                }
-
-                final RestTokenDataClient.TokenDestinationData inputTokenDestData = new RestTokenDataClient.TokenDestinationData(
-                        null, toNum, null );
-                final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmApplication );
-                final RestTokenDataClient.TokenDestinationData outputDestTokenData = restTokenDataClient.figureDestTokenDisplayString(
-                        pwmRequest.getSessionLabel(),
-                        inputTokenDestData,
-                        null,
-                        pwmRequest.getLocale() );
-                if ( outputDestTokenData == null || outputDestTokenData.getSms() == null || outputDestTokenData.getSms().isEmpty() )
-                {
-                    //avoid sending SMS code token
-                    break;
-                }
-                final String tokenKey;
-                try
-                {
-                    final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
-                            password.pwm.svc.token.TokenType.NEWUSER_SMS,
-                            newUserProfile.getTokenDurationSMS( config ),
-                            tokenPayloadMap,
-                            null,
-                            Collections.singleton( outputDestTokenData.getSms() )
-                    );
-                    tokenKey = pwmApplication.getTokenService().generateNewToken( tokenPayload,
-                            pwmRequest.getSessionLabel() );
-                }
-                catch ( PwmOperationalException e )
-                {
-                    throw new PwmUnrecoverableException( e.getErrorInformation() );
-                }
-
-                final String message = config.readSettingAsLocalizedString( PwmSetting.SMS_NEWUSER_TOKEN_TEXT,
-                        pwmSession.getSessionStateBean().getLocale() );
-
-                try
-                {
-                    TokenService.TokenSender.sendSmsToken(
-                            TokenService.TokenSendInfo.builder()
-                                    .pwmApplication( pwmApplication )
-                                    .userInfo( null )
-                                    .macroMachine( macroMachine )
-                                    .smsNumber( outputDestTokenData.getSms() )
-                                    .smsMessage( message )
-                                    .tokenKey( tokenKey )
-                                    .sessionLabel( pwmRequest.getSessionLabel() )
-                                    .build()
-                    );
-                }
-                catch ( Exception e )
-                {
-                    throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_UNKNOWN ) );
-                }
-
-                newUserBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.SMS );
-                final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( pwmApplication.getConfig() );
-                newUserBean.getTokenVerificationProgress().setTokenDisplayText( tokenDestinationDisplayMasker.maskPhone( toNum ) );
-                newUserBean.getTokenVerificationProgress().setPhase( TokenVerificationProgress.TokenChannel.SMS );
-            }
-            break;
-
-            case EMAIL:
-            {
-                final EmailItemBean configuredEmailSetting = config.readSettingAsEmail(
-                        PwmSetting.EMAIL_NEWUSER_VERIFICATION, pwmSession.getSessionStateBean().getLocale() );
-                final String toAddress = macroMachine.expandMacros( configuredEmailSetting.getTo() );
-
-                final RestTokenDataClient.TokenDestinationData inputTokenDestData = new RestTokenDataClient.TokenDestinationData(
-                        toAddress, null, null );
-                final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmApplication );
-                final RestTokenDataClient.TokenDestinationData outputDestTokenData = restTokenDataClient.figureDestTokenDisplayString(
-                        pwmRequest.getSessionLabel(),
-                        inputTokenDestData,
-                        null,
-                        pwmRequest.getLocale() );
-                if ( outputDestTokenData == null || outputDestTokenData.getEmail() == null || outputDestTokenData.getEmail().isEmpty() )
-                {
-                    //avoid sending Email code token
-                    break;
-                }
-                final String tokenKey;
-                try
-                {
-                    final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
-                            password.pwm.svc.token.TokenType.NEWUSER_EMAIL,
-                            newUserProfile.getTokenDurationEmail( config ),
-                            tokenPayloadMap,
-                            null,
-                            Collections.singleton( outputDestTokenData.getEmail() )
-                    );
-                    tokenKey = pwmApplication.getTokenService().generateNewToken( tokenPayload,
-                            pwmRequest.getSessionLabel() );
-                }
-                catch ( PwmOperationalException e )
-                {
-                    throw new PwmUnrecoverableException( e.getErrorInformation() );
-                }
-
-                newUserBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.EMAIL );
-                newUserBean.getTokenVerificationProgress().setPhase( TokenVerificationProgress.TokenChannel.EMAIL );
-                final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( pwmApplication.getConfig() );
-                newUserBean.getTokenVerificationProgress().setTokenDisplayText( tokenDestinationDisplayMasker.maskEmail( toAddress ) );
-
-                final EmailItemBean emailItemBean = new EmailItemBean(
-                        outputDestTokenData.getEmail(),
-                        configuredEmailSetting.getFrom(),
-                        configuredEmailSetting.getSubject(),
-                        configuredEmailSetting.getBodyPlain().replace( "%TOKEN%", tokenKey ),
-                        configuredEmailSetting.getBodyHtml().replace( "%TOKEN%", tokenKey ) );
-
-                try
-                {
-                    TokenService.TokenSender.sendEmailToken(
-                            TokenService.TokenSendInfo.builder()
-                                    .pwmApplication( pwmApplication )
-                                    .userInfo( null )
-                                    .macroMachine( macroMachine )
-                                    .configuredEmailSetting( emailItemBean )
-                                    .emailAddress( outputDestTokenData.getEmail() )
-                                    .tokenKey( tokenKey )
-                                    .sessionLabel( pwmRequest.getSessionLabel() )
-                                    .build()
-                    );
-                }
-                catch ( Exception e )
-                {
-                    throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_UNKNOWN ) );
-                }
-            }
-            break;
+        final MacroMachine.StringReplacer stringReplacer = tokenDestinationItem == null
+                ? null
+                : TokenUtil.makeTokenDestStringReplacer( tokenDestinationItem );
 
-            default:
-                newUserBean.getTokenVerificationProgress().setPhase( null );
-                JavaHelper.unhandledSwitchStatement( tokenType );
-        }
+        return MacroMachine.forUser( pwmApplication, sessionLabel, stubUserBean, stubLoginBean, stringReplacer );
     }
 
     static Map<String, String> figureDisplayableProfiles( final PwmRequest pwmRequest )
@@ -767,4 +600,142 @@ class NewUserUtils
         }
     }
 
+    static Map<String, TokenDestinationItem.Type> determineTokenValidationsRequired(
+            final PwmRequest pwmRequest,
+            final NewUserBean newUserBean,
+            final NewUserProfile newUserProfile
+    )
+            throws PwmUnrecoverableException
+    {
+        final List<FormConfiguration> formFields = newUserProfile.readSettingAsForm( PwmSetting.NEWUSER_FORM );
+        final LdapProfile defaultLDAPProfile = pwmRequest.getConfig().getDefaultLdapProfile();
+
+        final Map<String, TokenDestinationItem.Type> workingMap = new LinkedHashMap<>( FormUtility.identifyFormItemsNeedingPotentialTokenValidation(
+                defaultLDAPProfile,
+                formFields
+        ) );
+
+        final Set<TokenDestinationItem.Type> interestedTypes = new HashSet<>(  );
+        if ( newUserProfile.readSettingAsBoolean( PwmSetting.NEWUSER_EMAIL_VERIFICATION ) )
+        {
+            interestedTypes.add( TokenDestinationItem.Type.email );
+        }
+        if ( newUserProfile.readSettingAsBoolean( PwmSetting.NEWUSER_SMS_VERIFICATION ) )
+        {
+            interestedTypes.add( TokenDestinationItem.Type.sms );
+        }
+
+        if ( !JavaHelper.isEmpty( workingMap ) )
+        {
+            final Map<String, String> formData = newUserBean.getNewUserForm().getFormData();
+
+            for ( final Iterator<Map.Entry<String, TokenDestinationItem.Type>> iter = workingMap.entrySet().iterator(); iter.hasNext(); )
+            {
+                final Map.Entry<String, TokenDestinationItem.Type> entry = iter.next();
+                final String attrName = entry.getKey();
+                final TokenDestinationItem.Type type = entry.getValue();
+
+                if ( !interestedTypes.contains( type ) )
+                {
+                    iter.remove();
+                }
+                if ( !formData.containsKey( attrName ) )
+                {
+                    iter.remove();
+                }
+            }
+        }
+
+        return Collections.unmodifiableMap( workingMap );
+    }
+
+    static ProcessStatus checkForTokenVerificationProgress(
+            final PwmRequest pwmRequest,
+            final NewUserBean newUserBean,
+            final NewUserProfile newUserProfile
+    )
+            throws PwmUnrecoverableException, ServletException, IOException
+    {
+        final Map<String, TokenDestinationItem.Type> requiredTokenValidations = determineTokenValidationsRequired(
+                pwmRequest,
+                newUserBean,
+                newUserProfile
+        );
+
+        if ( !requiredTokenValidations.isEmpty() )
+        {
+            final Set<String> remainingValidations = new HashSet<>( requiredTokenValidations.keySet() );
+            remainingValidations.removeAll( newUserBean.getCompletedTokenFields() );
+
+            if ( !remainingValidations.isEmpty() )
+            {
+                if ( StringUtil.isEmpty( newUserBean.getCurrentTokenField() ) )
+                {
+                    newUserBean.setCurrentTokenField( remainingValidations.iterator().next() );
+                    newUserBean.setTokenSent( false );
+                }
+
+                if ( !newUserBean.isTokenSent() )
+                {
+                    final TokenDestinationItem tokenDestinationItem = tokenDestinationItemForCurrentValidation( pwmRequest, newUserBean, newUserProfile );
+
+                    if ( pwmRequest.getConfig().getTokenStorageMethod() == TokenStorageMethod.STORE_LDAP )
+                    {
+                        throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
+                                "cannot generate new user tokens when storage type is configured as STORE_LDAP.",
+                        } ) );
+                    }
+
+                    final Map<String, String> tokenPayloadMap = NewUserFormUtils.toTokenPayload( pwmRequest, newUserBean );
+                    final MacroMachine macroMachine = createMacroMachineForNewUser(
+                            pwmRequest.getPwmApplication(),
+                            pwmRequest.getSessionLabel(),
+                            newUserBean.getNewUserForm(),
+                            tokenDestinationItem );
+
+                    TokenUtil.initializeAndSendToken(
+                            pwmRequest,
+                            null,
+                            tokenDestinationItem,
+                            PwmSetting.EMAIL_NEWUSER_VERIFICATION,
+                            TokenType.NEWUSER,
+                            PwmSetting.SMS_NEWUSER_TOKEN_TEXT,
+                            tokenPayloadMap,
+                            macroMachine
+                    );
+                    newUserBean.setTokenSent( true );
+                }
+
+                NewUserServlet.forwardToEnterCode( pwmRequest, newUserProfile, newUserBean );
+                return ProcessStatus.Halt;
+            }
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    static TokenDestinationItem tokenDestinationItemForCurrentValidation(
+            final PwmRequest pwmRequest,
+            final NewUserBean newUserBean,
+            final NewUserProfile newUserProfile
+    )
+            throws PwmUnrecoverableException
+    {
+        final List<FormConfiguration> formFields = newUserProfile.readSettingAsForm( PwmSetting.NEWUSER_FORM );
+        final LdapProfile defaultLDAPProfile = pwmRequest.getConfig().getDefaultLdapProfile();
+
+        final Map<String, TokenDestinationItem.Type> tokenTypeMap = FormUtility.identifyFormItemsNeedingPotentialTokenValidation(
+                defaultLDAPProfile,
+                formFields
+        );
+
+        final String value = newUserBean.getNewUserForm().getFormData().get( newUserBean.getCurrentTokenField() );
+        final TokenDestinationItem.Type type = tokenTypeMap.get( newUserBean.getCurrentTokenField() );
+        return TokenDestinationItem.builder()
+                .display( value )
+                .id( "1" )
+                .value( value )
+                .type( type )
+                .build();
+    }
 }

+ 453 - 0
server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileServlet.java

@@ -0,0 +1,453 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.servlet.updateprofile;
+
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import lombok.Data;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.TokenDestinationItem;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.UpdateProfileProfile;
+import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmDataValidationException;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.JspUrl;
+import password.pwm.http.ProcessStatus;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.http.PwmSession;
+import password.pwm.http.bean.UpdateProfileBean;
+import password.pwm.http.servlet.ControlledPwmServlet;
+import password.pwm.i18n.Message;
+import password.pwm.svc.event.AuditEvent;
+import password.pwm.svc.event.AuditRecord;
+import password.pwm.svc.event.AuditRecordFactory;
+import password.pwm.svc.token.TokenService;
+import password.pwm.svc.token.TokenType;
+import password.pwm.svc.token.TokenUtil;
+import password.pwm.util.form.FormUtility;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.ws.server.RestResultBean;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * User interaction servlet for updating user attributes.
+ *
+ * @author Jason D. Rivard
+ */
+@WebServlet(
+        name = "UpdateProfileServlet",
+        urlPatterns = {
+                PwmConstants.URL_PREFIX_PRIVATE + "/updateprofile",
+                PwmConstants.URL_PREFIX_PRIVATE + "/UpdateProfile"
+        }
+)
+public class UpdateProfileServlet extends ControlledPwmServlet
+{
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass( UpdateProfileServlet.class );
+
+    @Data
+    public static class ValidateResponse implements Serializable
+    {
+        private String message;
+        private boolean success;
+    }
+
+    public enum UpdateProfileAction implements ProcessAction
+    {
+        updateProfile( HttpMethod.POST ),
+        agree( HttpMethod.POST ),
+        confirm( HttpMethod.POST ),
+        unConfirm( HttpMethod.POST ),
+        validate( HttpMethod.POST ),
+        enterCode( HttpMethod.POST ),;
+
+        private final HttpMethod method;
+
+        UpdateProfileAction( final HttpMethod method )
+        {
+            this.method = method;
+        }
+
+        public Collection<HttpMethod> permittedMethods( )
+        {
+            return Collections.singletonList( method );
+        }
+    }
+
+    @Override
+    public Class<? extends ProcessAction> getProcessActionsClass( )
+    {
+        return UpdateProfileAction.class;
+    }
+
+    private static UpdateProfileProfile getProfile( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    {
+        return pwmRequest.getPwmSession().getSessionManager().getUpdateAttributeProfile( pwmRequest.getPwmApplication() );
+    }
+
+    private static UpdateProfileBean getBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    {
+        return pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, UpdateProfileBean.class );
+    }
+
+    @ActionHandler( action = "enterCode" )
+    ProcessStatus handleEnterCodeRequest(
+            final PwmRequest pwmRequest
+    )
+            throws PwmUnrecoverableException, ServletException, IOException
+    {
+        setLastError( pwmRequest, null );
+
+        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
+        final UpdateProfileProfile updateProfileProfile = getProfile( pwmRequest );
+
+        final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
+
+        final TokenDestinationItem tokenDestinationItem = UpdateProfileUtil.tokenDestinationItemForCurrentValidation(
+                pwmRequest,
+                updateProfileBean,
+                updateProfileProfile
+        );
+
+        ErrorInformation errorInformation = null;
+        try
+        {
+            TokenUtil.checkEnteredCode(
+                    pwmRequest,
+                    userEnteredCode,
+                    tokenDestinationItem,
+                    pwmRequest.getUserInfoIfLoggedIn(),
+                    TokenType.UPDATE,
+                    TokenService.TokenEntryType.authenticated
+            );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.debug( pwmRequest, "error while checking entered token: " );
+            errorInformation = e.getErrorInformation();
+        }
+
+        if ( errorInformation != null )
+        {
+            setLastError( pwmRequest, errorInformation );
+            UpdateProfileUtil.forwardToEnterCode( pwmRequest, updateProfileProfile, updateProfileBean );
+            return ProcessStatus.Halt;
+        }
+
+        LOGGER.debug( pwmRequest, "marking token as passed " + JsonUtil.serialize( tokenDestinationItem ) );
+        updateProfileBean.getCompletedTokenFields().add( updateProfileBean.getCurrentTokenField() );
+        updateProfileBean.setTokenSent( false );
+        updateProfileBean.setCurrentTokenField( null );
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "validate" )
+    ProcessStatus restValidateForm(
+            final PwmRequest pwmRequest
+    )
+            throws IOException, ServletException, PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
+        final UpdateProfileProfile updateProfileProfile = getProfile( pwmRequest );
+
+        boolean success = true;
+        String userMessage = Message.getLocalizedMessage( pwmRequest.getLocale(), Message.Success_UpdateForm, pwmRequest.getConfig() );
+
+        try
+        {
+            // read in the responses from the request
+            final Map<FormConfiguration, String> formValues = UpdateProfileUtil.readFromJsonRequest( pwmRequest, updateProfileProfile, updateProfileBean );
+
+            // verify form meets the form requirements
+            UpdateProfileUtil.verifyFormAttributes( pwmRequest.getPwmApplication(), pwmRequest.getUserInfoIfLoggedIn(), pwmRequest.getLocale(), formValues, true );
+
+            updateProfileBean.getFormData().putAll( FormUtility.asStringMap( formValues ) );
+        }
+        catch ( PwmOperationalException e )
+        {
+            success = false;
+            userMessage = e.getErrorInformation().toUserStr( pwmRequest.getPwmSession(), pwmRequest.getPwmApplication() );
+        }
+
+        final ValidateResponse response = new ValidateResponse();
+        response.setMessage( userMessage );
+        response.setSuccess( success );
+        pwmRequest.outputJsonResult( RestResultBean.withData( response ) );
+        return ProcessStatus.Halt;
+    }
+
+    @ActionHandler( action = "unConfirm" )
+    ProcessStatus handleUnconfirm(
+            final PwmRequest pwmRequest
+    )
+            throws PwmUnrecoverableException
+    {
+        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
+
+        updateProfileBean.setFormSubmitted( false );
+        updateProfileBean.setConfirmationPassed( false );
+        updateProfileBean.getCompletedTokenFields().clear();
+        updateProfileBean.setTokenSent( false );
+        updateProfileBean.setCurrentTokenField( null );
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "agree" )
+    ProcessStatus handleAgreeRequest( final PwmRequest pwmRequest )
+            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException
+    {
+        LOGGER.debug( pwmRequest, "user accepted agreement" );
+
+        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
+        if ( !updateProfileBean.isAgreementPassed() )
+        {
+            updateProfileBean.setAgreementPassed( true );
+            final AuditRecord auditRecord = new AuditRecordFactory( pwmRequest ).createUserAuditRecord(
+                    AuditEvent.AGREEMENT_PASSED,
+                    pwmRequest.getUserInfoIfLoggedIn(),
+                    pwmRequest.getSessionLabel(),
+                    "UpdateProfile"
+            );
+            pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "confirm" )
+    ProcessStatus handleConfirmRequest( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
+        updateProfileBean.setConfirmationPassed( true );
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "updateProfile" )
+    ProcessStatus handleUpdateProfileRequest(
+            final PwmRequest pwmRequest
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
+        final UpdateProfileProfile updateProfileProfile = getProfile( pwmRequest );
+
+        try
+        {
+            readFormParametersFromRequest( pwmRequest, updateProfileProfile, updateProfileBean );
+        }
+        catch ( PwmOperationalException e )
+        {
+            LOGGER.error( pwmRequest, e.getMessage() );
+            setLastError( pwmRequest, e.getErrorInformation() );
+        }
+
+        updateProfileBean.setFormSubmitted( true );
+
+        return ProcessStatus.Continue;
+    }
+
+    protected void nextStep( final PwmRequest pwmRequest )
+            throws IOException, ServletException, PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
+        final UpdateProfileProfile updateProfileProfile = getProfile( pwmRequest );
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+
+        {
+            final String updateProfileAgreementText = updateProfileProfile.readSettingAsLocalizedString(
+                    PwmSetting.UPDATE_PROFILE_AGREEMENT_MESSAGE,
+                    pwmSession.getSessionStateBean().getLocale()
+            );
+
+            if ( !StringUtil.isEmpty( updateProfileAgreementText ) )
+            {
+                if ( !updateProfileBean.isAgreementPassed() )
+                {
+                    final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( pwmRequest.getPwmApplication() );
+                    final String expandedText = macroMachine.expandMacros( updateProfileAgreementText );
+                    pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
+                    pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES_AGREEMENT );
+                    return;
+                }
+            }
+        }
+
+        //make sure there is form data in the bean.
+        if ( !updateProfileBean.isFormLdapLoaded() )
+        {
+            updateProfileBean.getFormData().clear();
+            updateProfileBean.getFormData().putAll( ( UpdateProfileUtil.formDataFromLdap( pwmRequest, updateProfileProfile ) ) );
+            updateProfileBean.setFormLdapLoaded( true );
+            UpdateProfileUtil.forwardToForm( pwmRequest, updateProfileProfile, updateProfileBean );
+            return;
+        }
+
+        if ( !updateProfileBean.isFormSubmitted() )
+        {
+            UpdateProfileUtil.forwardToForm( pwmRequest, updateProfileProfile, updateProfileBean );
+            return;
+        }
+
+
+        // validate the form data.
+        try
+        {
+            // verify form meets the form requirements
+            final List<FormConfiguration> formFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+            final Map<FormConfiguration, String> formValues = FormUtility.readFormValuesFromMap( updateProfileBean.getFormData(), formFields, pwmRequest.getLocale() );
+            UpdateProfileUtil.verifyFormAttributes( pwmRequest.getPwmApplication(), pwmRequest.getUserInfoIfLoggedIn(), pwmRequest.getLocale(), formValues, true );
+        }
+        catch ( PwmException e )
+        {
+            LOGGER.error( pwmSession, e.getMessage() );
+            setLastError( pwmRequest, e.getErrorInformation() );
+            UpdateProfileUtil.forwardToForm( pwmRequest, updateProfileProfile, updateProfileBean );
+            return;
+        }
+
+        {
+            final boolean requireConfirmation = updateProfileProfile.readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_SHOW_CONFIRMATION );
+            if ( requireConfirmation && !updateProfileBean.isConfirmationPassed() )
+            {
+                UpdateProfileUtil.forwardToConfirmForm( pwmRequest, updateProfileProfile, updateProfileBean );
+                return;
+            }
+        }
+
+        if ( UpdateProfileUtil.checkForTokenVerificationProgress( pwmRequest, updateProfileBean, updateProfileProfile ) == ProcessStatus.Halt )
+        {
+            return;
+        }
+
+        try
+        {
+            // write the form values
+            final ChaiUser theUser = pwmSession.getSessionManager().getActor( pwmApplication );
+            UpdateProfileUtil.doProfileUpdate(
+                    pwmRequest.getPwmApplication(),
+                    pwmRequest.getSessionLabel(),
+                    pwmRequest.getLocale(),
+                    pwmSession.getUserInfo(),
+                    pwmSession.getSessionManager().getMacroMachine( pwmApplication ),
+                    updateProfileProfile,
+                    updateProfileBean.getFormData(),
+                    theUser
+            );
+
+            // re-populate the uiBean because we have changed some values.
+            pwmSession.reloadUserInfoBean( pwmApplication );
+
+            // clear cached read attributes.
+            pwmRequest.getPwmSession().reloadUserInfoBean( pwmApplication );
+
+            // mark the event log
+            pwmApplication.getAuditManager().submit( AuditEvent.UPDATE_PROFILE, pwmSession.getUserInfo(), pwmSession );
+
+            // clear the bean
+            pwmApplication.getSessionStateService().clearBean( pwmRequest, UpdateProfileBean.class );
+
+            pwmRequest.getPwmResponse().forwardToSuccessPage( Message.Success_UpdateProfile );
+            return;
+        }
+        catch ( PwmException e )
+        {
+            LOGGER.error( pwmSession, e.getMessage() );
+            setLastError( pwmRequest, e.getErrorInformation() );
+        }
+        catch ( ChaiException e )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UPDATE_ATTRS_FAILURE, e.toString() );
+            LOGGER.error( pwmSession, errorInformation.toDebugStr() );
+            setLastError( pwmRequest, errorInformation );
+        }
+
+        UpdateProfileUtil.forwardToForm( pwmRequest, updateProfileProfile, updateProfileBean );
+    }
+
+    @Override
+    public ProcessStatus preProcessCheck( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException
+    {
+        if ( !pwmRequest.getPwmApplication().getConfig().readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_ENABLE ) )
+        {
+            pwmRequest.respondWithError( new ErrorInformation(
+                    PwmError.ERROR_SERVICE_NOT_AVAILABLE,
+                    "Setting " + PwmSetting.UPDATE_PROFILE_ENABLE.toMenuLocationDebug( null, null ) + " is not enabled." )
+            );
+            return ProcessStatus.Halt;
+        }
+
+        final UpdateProfileProfile updateProfileProfile = getProfile( pwmRequest );
+        if ( updateProfileProfile == null )
+        {
+            pwmRequest.respondWithError( new ErrorInformation( PwmError.ERROR_NO_PROFILE_ASSIGNED ) );
+            return ProcessStatus.Halt;
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+
+    final Map<FormConfiguration, String> readFormParametersFromRequest(
+            final PwmRequest pwmRequest,
+            final UpdateProfileProfile updateProfileProfile,
+            final UpdateProfileBean updateProfileBean
+    )
+            throws PwmUnrecoverableException, PwmDataValidationException, ChaiUnavailableException
+    {
+        final List<FormConfiguration> formFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+
+        //read the values from the request
+        final Map<FormConfiguration, String> formValueMap = FormUtility.readFormValuesFromRequest( pwmRequest, formFields, pwmRequest.getLocale() );
+
+        return UpdateProfileUtil.updateBeanFormData( formFields, formValueMap, updateProfileBean );
+    }
+
+
+}
+

+ 407 - 0
server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java

@@ -0,0 +1,407 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.servlet.updateprofile;
+
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.PwmApplication;
+import password.pwm.bean.EmailItemBean;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.TokenDestinationItem;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.LdapProfile;
+import password.pwm.config.profile.UpdateProfileProfile;
+import password.pwm.config.value.data.ActionConfiguration;
+import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.error.PwmDataValidationException;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.JspUrl;
+import password.pwm.http.ProcessStatus;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.http.bean.UpdateProfileBean;
+import password.pwm.ldap.LdapOperationsHelper;
+import password.pwm.ldap.UserInfo;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.token.TokenType;
+import password.pwm.svc.token.TokenUtil;
+import password.pwm.util.form.FormUtility;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.operations.ActionExecutor;
+
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+public class UpdateProfileUtil
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( UpdateProfileUtil.class );
+
+    private UpdateProfileUtil()
+    {
+    }
+
+    static Map<FormConfiguration, String> readFromJsonRequest(
+            final PwmRequest pwmRequest,
+            final UpdateProfileProfile updateProfileProfile,
+            final UpdateProfileBean updateProfileBean
+    )
+            throws PwmDataValidationException, PwmUnrecoverableException, IOException
+    {
+        final List<FormConfiguration> formFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+
+        final Map<FormConfiguration, String> formValueMap = FormUtility.readFormValuesFromMap( pwmRequest.readBodyAsJsonStringMap(), formFields, pwmRequest.getLocale() );
+
+        return updateBeanFormData( formFields, formValueMap, updateProfileBean );
+    }
+
+    static Map<FormConfiguration, String> updateBeanFormData(
+            final List<FormConfiguration> formFields,
+            final Map<FormConfiguration, String> formValueMap,
+            final UpdateProfileBean updateProfileBean
+    )
+    {
+        final LinkedHashMap<FormConfiguration, String> newFormValueMap = new LinkedHashMap<>();
+        for ( final FormConfiguration formConfiguration : formFields )
+        {
+            if ( formConfiguration.isReadonly() )
+            {
+                final String existingValue = updateProfileBean.getFormData().get( formConfiguration.getName() );
+                newFormValueMap.put( formConfiguration, existingValue );
+            }
+            else
+            {
+                newFormValueMap.put( formConfiguration, formValueMap.get( formConfiguration ) );
+            }
+        }
+
+        updateProfileBean.getFormData().clear();
+        updateProfileBean.getFormData().putAll( FormUtility.asStringMap( newFormValueMap ) );
+
+        return newFormValueMap;
+    }
+
+    static void verifyFormAttributes(
+            final PwmApplication pwmApplication,
+            final UserIdentity userIdentity,
+            final Locale userLocale,
+            final Map<FormConfiguration, String> formValues,
+            final boolean allowResultCaching
+    )
+            throws PwmOperationalException, PwmUnrecoverableException
+    {
+        // see if the values meet form requirements.
+        FormUtility.validateFormValues( pwmApplication.getConfig(), formValues, userLocale );
+
+        final List<FormUtility.ValidationFlag> validationFlags = new ArrayList<>();
+        if ( allowResultCaching )
+        {
+            validationFlags.add( FormUtility.ValidationFlag.allowResultCaching );
+        }
+
+        // check unique fields against ldap
+        FormUtility.validateFormValueUniqueness(
+                pwmApplication,
+                formValues,
+                userLocale,
+                Collections.singletonList( userIdentity ),
+                validationFlags.toArray( new FormUtility.ValidationFlag[ validationFlags.size() ] )
+        );
+    }
+
+    static void sendProfileUpdateEmailNotice(
+            final PwmApplication pwmApplication,
+            final MacroMachine macroMachine,
+            final UserInfo userInfo,
+            final Locale locale,
+            final SessionLabel sessionLabel
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final Configuration config = pwmApplication.getConfig();
+
+        final EmailItemBean configuredEmailSetting = config.readSettingAsEmail( PwmSetting.EMAIL_UPDATEPROFILE, locale );
+        pwmApplication.getEmailQueue().submitEmail(
+                configuredEmailSetting,
+                userInfo,
+                macroMachine
+        );
+
+        if ( configuredEmailSetting == null )
+        {
+            LOGGER.debug( sessionLabel, "skipping send profile update email for '" + userInfo.getUserIdentity().toDisplayString() + "' no email configured" );
+        }
+    }
+
+    static void forwardToForm( final PwmRequest pwmRequest, final UpdateProfileProfile updateProfileProfile, final UpdateProfileBean updateProfileBean )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        final List<FormConfiguration> form = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final Map<FormConfiguration, String> formValueMap = formMapFromBean( updateProfileProfile, updateProfileBean );
+        pwmRequest.addFormInfoToRequestAttr( form, formValueMap, false, false );
+        final List<FormConfiguration> links = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_CUSTOMLINKS );
+        pwmRequest.setAttribute( PwmRequestAttribute.FormCustomLinks, new ArrayList<>( links ) );
+        pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES );
+    }
+
+    static void forwardToEnterCode( final PwmRequest pwmRequest, final UpdateProfileProfile updateProfileProfile, final UpdateProfileBean updateProfileBean )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        final TokenDestinationItem tokenDestinationItem = tokenDestinationItemForCurrentValidation(
+                pwmRequest,
+                updateProfileBean,
+                updateProfileProfile );
+        pwmRequest.setAttribute( PwmRequestAttribute.TokenDestItems, tokenDestinationItem );
+        pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES_ENTER_CODE );
+    }
+
+    static void forwardToConfirmForm( final PwmRequest pwmRequest, final UpdateProfileProfile updateProfileProfile, final UpdateProfileBean updateProfileBean )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        final List<FormConfiguration> form = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final Map<FormConfiguration, String> formValueMap = formMapFromBean( updateProfileProfile, updateProfileBean );
+        pwmRequest.addFormInfoToRequestAttr( form, formValueMap, true, false );
+        pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES_CONFIRM );
+    }
+
+    static Map<FormConfiguration, String> formMapFromBean(
+            final UpdateProfileProfile updateProfileProfile,
+            final UpdateProfileBean updateProfileBean
+    )
+            throws PwmUnrecoverableException
+    {
+
+        final List<FormConfiguration> form = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final Map<FormConfiguration, String> formValueMap = new LinkedHashMap<>();
+        for ( final FormConfiguration formConfiguration : form )
+        {
+            formValueMap.put(
+                    formConfiguration,
+                    updateProfileBean.getFormData().keySet().contains( formConfiguration.getName() )
+                            ? updateProfileBean.getFormData().get( formConfiguration.getName() )
+                            : ""
+            );
+        }
+        return formValueMap;
+    }
+
+    static Map<String, String> formDataFromLdap( final PwmRequest pwmRequest, final UpdateProfileProfile updateProfileProfile )
+            throws PwmUnrecoverableException
+    {
+        final UserInfo userInfo = pwmRequest.getPwmSession().getUserInfo();
+        final List<FormConfiguration> formFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final Map<FormConfiguration, String> formMap = new LinkedHashMap<>();
+        FormUtility.populateFormMapFromLdap( formFields, pwmRequest.getSessionLabel(), formMap, userInfo );
+        return FormUtility.asStringMap( formMap );
+    }
+
+    static Map<String, TokenDestinationItem.Type> determineTokenValidationsRequired(
+            final PwmRequest pwmRequest,
+            final UpdateProfileBean updateProfileBean,
+            final UpdateProfileProfile updateProfileProfile
+    )
+            throws PwmUnrecoverableException
+    {
+        final List<FormConfiguration> formFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final LdapProfile ldapProfile = pwmRequest.getUserInfoIfLoggedIn().getLdapProfile( pwmRequest.getConfig() );
+        final Map<String, TokenDestinationItem.Type> workingMap = new LinkedHashMap<>( FormUtility.identifyFormItemsNeedingPotentialTokenValidation(
+                ldapProfile,
+                formFields
+        ) );
+
+        final Set<TokenDestinationItem.Type> interestedTypes = new HashSet<>(  );
+        if ( updateProfileProfile.readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_EMAIL_VERIFICATION ) )
+        {
+            interestedTypes.add( TokenDestinationItem.Type.email );
+        }
+        if ( updateProfileProfile.readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_SMS_VERIFICATION ) )
+        {
+            interestedTypes.add( TokenDestinationItem.Type.sms );
+        }
+
+        if ( !JavaHelper.isEmpty( workingMap ) )
+        {
+            final Map<String, String> ldapData = formDataFromLdap( pwmRequest, updateProfileProfile );
+            final Map<String, String> updateData = updateProfileBean.getFormData();
+
+            for ( final Iterator<Map.Entry<String, TokenDestinationItem.Type>> iter = workingMap.entrySet().iterator(); iter.hasNext(); )
+            {
+                final Map.Entry<String, TokenDestinationItem.Type> entry = iter.next();
+                final String attrName = entry.getKey();
+                final TokenDestinationItem.Type type = entry.getValue();
+
+                if ( !interestedTypes.contains( type ) )
+                {
+                    iter.remove();
+                }
+                else if ( updateData.containsKey( attrName ) )
+                {
+                    final String updateValue = updateData.get( attrName );
+                    final String ldapValue = ldapData.get( attrName );
+                    if ( StringUtil.nullSafeEqualsIgnoreCase( updateValue, ldapValue ) )
+                    {
+                        iter.remove();
+                    }
+                }
+            }
+        }
+
+        return Collections.unmodifiableMap( workingMap );
+    }
+
+
+    static ProcessStatus checkForTokenVerificationProgress(
+            final PwmRequest pwmRequest,
+            final UpdateProfileBean updateProfileBean,
+            final UpdateProfileProfile updateProfileProfile
+    )
+            throws PwmUnrecoverableException, ServletException, IOException
+    {
+        final Map<String, TokenDestinationItem.Type> requiredTokenValidations = determineTokenValidationsRequired(
+                pwmRequest,
+                updateProfileBean,
+                updateProfileProfile
+        );
+
+        if ( !requiredTokenValidations.isEmpty() )
+        {
+            final Set<String> remainingValidations = new HashSet<>( requiredTokenValidations.keySet() );
+            remainingValidations.removeAll( updateProfileBean.getCompletedTokenFields() );
+
+            if ( !remainingValidations.isEmpty() )
+            {
+                if ( StringUtil.isEmpty( updateProfileBean.getCurrentTokenField() ) )
+                {
+                    updateProfileBean.setCurrentTokenField( remainingValidations.iterator().next() );
+                    updateProfileBean.setTokenSent( false );
+                }
+
+                if ( !updateProfileBean.isTokenSent() )
+                {
+                    final TokenDestinationItem tokenDestinationItem = tokenDestinationItemForCurrentValidation( pwmRequest, updateProfileBean, updateProfileProfile );
+
+                    TokenUtil.initializeAndSendToken(
+                            pwmRequest,
+                            pwmRequest.getPwmSession().getUserInfo(),
+                            tokenDestinationItem,
+                            PwmSetting.EMAIL_UPDATEPROFILE_VERIFICATION,
+                            TokenType.UPDATE,
+                            PwmSetting.SMS_UPDATE_PROFILE_TOKEN_TEXT
+                    );
+                    updateProfileBean.setTokenSent( true );
+
+                }
+
+                forwardToEnterCode( pwmRequest, updateProfileProfile, updateProfileBean );
+                return ProcessStatus.Halt;
+            }
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @SuppressWarnings( "checkstyle:ParameterNumber" )
+    public static void doProfileUpdate(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final Locale locale,
+            final UserInfo userInfo,
+            final MacroMachine macroMachine,
+            final UpdateProfileProfile updateProfileProfile,
+            final Map<String, String> formValues,
+            final ChaiUser theUser
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
+    {
+        final List<FormConfiguration> formFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final Map<FormConfiguration, String> formMap = FormUtility.readFormValuesFromMap( formValues, formFields, locale );
+
+        // verify form meets the form requirements (may be redundant, but shouldn't hurt)
+        verifyFormAttributes( pwmApplication, userInfo.getUserIdentity(), locale, formMap, false );
+
+        // write values.
+        LOGGER.info( "updating profile for " + userInfo.getUserIdentity() );
+
+        LdapOperationsHelper.writeFormValuesToLdap( pwmApplication, macroMachine, theUser, formMap, false );
+
+        final UserIdentity userIdentity = userInfo.getUserIdentity();
+
+        {
+            // execute configured actions
+            final List<ActionConfiguration> actions = updateProfileProfile.readSettingAsAction( PwmSetting.UPDATE_PROFILE_WRITE_ATTRIBUTES );
+            if ( actions != null && !actions.isEmpty() )
+            {
+                LOGGER.debug( sessionLabel, "executing configured actions to user " + userIdentity );
+
+
+                final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
+                        .setExpandPwmMacros( true )
+                        .setMacroMachine( macroMachine )
+                        .createActionExecutor();
+
+                actionExecutor.executeActions( actions, sessionLabel );
+            }
+        }
+        sendProfileUpdateEmailNotice( pwmApplication, macroMachine, userInfo, locale, sessionLabel );
+
+        // success, so forward to success page
+        pwmApplication.getStatisticsManager().incrementValue( Statistic.UPDATE_ATTRIBUTES );
+    }
+
+    static TokenDestinationItem tokenDestinationItemForCurrentValidation(
+            final PwmRequest pwmRequest,
+            final UpdateProfileBean updateProfileBean,
+            final UpdateProfileProfile updateProfileProfile
+    )
+    {
+        final List<FormConfiguration> formFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final LdapProfile ldapProfile = pwmRequest.getUserInfoIfLoggedIn().getLdapProfile( pwmRequest.getConfig() );
+        final Map<String, TokenDestinationItem.Type> tokenTypeMap = FormUtility.identifyFormItemsNeedingPotentialTokenValidation(
+                ldapProfile,
+                formFields
+        );
+
+        final String value = updateProfileBean.getFormData().get( updateProfileBean.getCurrentTokenField() );
+        final TokenDestinationItem.Type type = tokenTypeMap.get( updateProfileBean.getCurrentTokenField() );
+        return TokenDestinationItem.builder()
+                .display( value )
+                .id( "1" )
+                .value( value )
+                .type( type )
+                .build();
+    }
+}

+ 6 - 6
server/src/main/java/password/pwm/ldap/UserInfoReader.java

@@ -44,7 +44,7 @@ import password.pwm.config.profile.ProfileUtility;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.profile.SetupOtpProfile;
-import password.pwm.config.profile.UpdateAttributesProfile;
+import password.pwm.config.profile.UpdateProfileProfile;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
@@ -475,25 +475,25 @@ public class UserInfoReader implements UserInfo
             return false;
         }
 
-        UpdateAttributesProfile updateAttributesProfile = null;
+        UpdateProfileProfile updateProfileProfile = null;
         final Map<ProfileType, String> profileIDs = selfCachedReference.getProfileIDs();
         if ( profileIDs.containsKey( ProfileType.UpdateAttributes ) )
         {
-            updateAttributesProfile = configuration.getUpdateAttributesProfile().get( profileIDs.get( ProfileType.UpdateAttributes ) );
+            updateProfileProfile = configuration.getUpdateAttributesProfile().get( profileIDs.get( ProfileType.UpdateAttributes ) );
         }
 
-        if ( updateAttributesProfile == null )
+        if ( updateProfileProfile == null )
         {
             return false;
         }
 
-        if ( !updateAttributesProfile.readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_FORCE_SETUP ) )
+        if ( !updateProfileProfile.readSettingAsBoolean( PwmSetting.UPDATE_PROFILE_FORCE_SETUP ) )
         {
             LOGGER.debug( sessionLabel, "checkProfiles: " + userIdentity.toString() + " profile force setup is not enabled" );
             return false;
         }
 
-        final List<FormConfiguration> updateFormFields = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final List<FormConfiguration> updateFormFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
 
         // populate the map from ldap
 

+ 4 - 4
server/src/main/java/password/pwm/svc/token/TokenPayload.java

@@ -33,7 +33,6 @@ import java.time.Instant;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Set;
 
 @Getter
 public class TokenPayload implements Serializable
@@ -52,7 +51,8 @@ public class TokenPayload implements Serializable
     @SerializedName( "user" )
     private final UserIdentity userIdentity;
 
-    private final Set<String> dest;
+    @SerializedName( "d" )
+    private final String destination;
 
     @SerializedName( "g" )
     private final String guid;
@@ -62,7 +62,7 @@ public class TokenPayload implements Serializable
             final Instant expiration,
             final Map<String, String> data,
             final UserIdentity user,
-            final Set<String> dest,
+            final String destination,
             final String guid
     )
     {
@@ -71,7 +71,7 @@ public class TokenPayload implements Serializable
         this.data = data == null ? Collections.emptyMap() : Collections.unmodifiableMap( data );
         this.name = name;
         this.userIdentity = user;
-        this.dest = dest == null ? Collections.emptySet() : Collections.unmodifiableSet( dest );
+        this.destination = destination;
         this.guid = guid;
     }
 

+ 13 - 10
server/src/main/java/password/pwm/svc/token/TokenService.java

@@ -76,7 +76,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.TimerTask;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
@@ -110,6 +109,12 @@ public class TokenService implements PwmService
 
     private boolean verifyPwModifyTime = true;
 
+    public enum TokenEntryType
+    {
+        unauthenticated,
+        authenticated,
+    }
+
     public TokenService( )
             throws PwmOperationalException
     {
@@ -120,7 +125,7 @@ public class TokenService implements PwmService
             final TimeDuration lifetime,
             final Map<String, String> data,
             final UserIdentity userIdentity,
-            final Set<String> dest
+            final String destination
     )
     {
         final long count = counter.getAndUpdate( operand ->
@@ -145,7 +150,7 @@ public class TokenService implements PwmService
             LOGGER.error( "error making payload guid: " + e.getMessage(), e );
         }
         final Instant expiration = lifetime.incrementFromInstant( Instant.now() );
-        return new TokenPayload( name.name(), expiration, data, userIdentity, dest, guid.toString() );
+        return new TokenPayload( name.name(), expiration, data, userIdentity, destination, guid.toString() );
     }
 
     public void init( final PwmApplication pwmApplication )
@@ -570,7 +575,8 @@ public class TokenService implements PwmService
             final PwmSession pwmSession,
             final UserIdentity sessionUserIdentity,
             final TokenType tokenType,
-            final String userEnteredCode
+            final String userEnteredCode,
+            final TokenEntryType tokenEntryType
     )
             throws PwmOperationalException, PwmUnrecoverableException
     {
@@ -582,12 +588,9 @@ public class TokenService implements PwmService
                     tokenType,
                     userEnteredCode
             );
-            if ( tokenPayload.getDest() != null )
+            if ( !StringUtil.isEmpty( tokenPayload.getDestination() ) )
             {
-                for ( final String dest : tokenPayload.getDest() )
-                {
-                    pwmApplication.getIntruderManager().clear( RecordType.TOKEN_DEST, dest );
-                }
+                pwmApplication.getIntruderManager().clear( RecordType.TOKEN_DEST, tokenPayload.getDestination() );
             }
             markTokenAsClaimed( tokenMachine.keyFromKey( userEnteredCode ), pwmSession, tokenPayload );
             return tokenPayload;
@@ -606,7 +609,7 @@ public class TokenService implements PwmService
 
             LOGGER.debug( pwmSession, errorInformation.toDebugStr() );
 
-            if ( sessionUserIdentity != null )
+            if ( sessionUserIdentity != null && tokenEntryType == TokenEntryType.unauthenticated )
             {
                 final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator( pwmApplication, pwmSession, null );
                 sessionAuthenticator.simulateBadPassword( sessionUserIdentity );

+ 2 - 4
server/src/main/java/password/pwm/svc/token/TokenType.java

@@ -31,10 +31,8 @@ public enum TokenType
 {
     FORGOTTEN_PW( "password.pwm.servlet.ForgottenPasswordServlet" ),
     ACTIVATION( "password.pwm.servlet.ActivateUserServlet" ),
-    UPDATE_SMS(),
-    UPDATE_EMAIL(),
-    NEWUSER_SMS( "password.pwm.servlet.NewUserServlet_SMS" ),
-    NEWUSER_EMAIL( "password.pwm.servlet.NewUserServlet_EMAIL" ),;
+    UPDATE( ),
+    NEWUSER( ),;
 
     private final Set<String> otherNames;
 

+ 97 - 12
server/src/main/java/password/pwm/svc/token/TokenUtil.java

@@ -38,6 +38,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.ldap.UserInfo;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
@@ -138,6 +139,67 @@ public class TokenUtil
                 .build();
     }
 
+    public static TokenPayload checkEnteredCode(
+            final PwmRequest pwmRequest,
+            final String userEnteredCode,
+            final TokenDestinationItem tokenDestinationItem,
+            final UserIdentity userIdentity,
+            final TokenType tokenType,
+            final TokenService.TokenEntryType tokenEntryType
+    )
+            throws PwmUnrecoverableException
+    {
+        try
+        {
+            final TokenPayload tokenPayload = pwmRequest.getPwmApplication().getTokenService().processUserEnteredCode(
+                    pwmRequest.getPwmSession(),
+                    pwmRequest.getUserInfoIfLoggedIn(),
+                    tokenType,
+                    userEnteredCode,
+                    tokenEntryType
+            );
+            if ( tokenPayload != null )
+            {
+                if ( !tokenType.matchesName( tokenPayload.getName() ) )
+                {
+                    final String errorMsg = "expecting email token type but received : " + tokenPayload.getName();
+                    throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
+                }
+
+                if ( tokenEntryType == TokenService.TokenEntryType.authenticated )
+                {
+                    if ( tokenPayload.getUserIdentity() == null )
+                    {
+                        final String errorMsg = "missing userID for received token";
+                        throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
+                    }
+
+                    if ( !userIdentity.canonicalEquals( pwmRequest.getUserInfoIfLoggedIn(), pwmRequest.getPwmApplication() ) )
+                    {
+                        final String errorMsg = "received token is not for currently authenticated user, received token is for: "
+                                + tokenPayload.getUserIdentity().toDisplayString();
+                        throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
+                    }
+                }
+
+                final String currentTokenDest = tokenDestinationItem.getValue();
+                final String payloadTokenDest = tokenPayload.getDestination();
+                if ( !StringUtil.nullSafeEquals( currentTokenDest, payloadTokenDest ) )
+                {
+                    final String errorMsg = "token is for destination '" + currentTokenDest
+                            + "', but the current expected destination is '" + payloadTokenDest + "'";
+                    throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
+                }
+            }
+
+            return tokenPayload;
+        }
+        catch ( PwmOperationalException e )
+        {
+            final String errorMsg = "token incorrect: " + e.getMessage();
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
+        }
+    }
 
     public static void initializeAndSendToken(
             final PwmRequest pwmRequest,
@@ -146,14 +208,31 @@ public class TokenUtil
             final PwmSetting emailToSend,
             final TokenType tokenType,
             final PwmSetting smsToSend
+    )
+            throws PwmUnrecoverableException
+    {
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userInfo.getUserIdentity(), makeTokenDestStringReplacer( tokenDestinationItem ) );
+        initializeAndSendToken( pwmRequest, userInfo, tokenDestinationItem, emailToSend, tokenType, smsToSend, Collections.emptyMap(), macroMachine );
+    }
 
+    @SuppressWarnings( "checkstyle:ParameterNumber" )
+    public static void initializeAndSendToken(
+            final PwmRequest pwmRequest,
+            final UserInfo userInfo,
+            final TokenDestinationItem tokenDestinationItem,
+            final PwmSetting emailToSend,
+            final TokenType tokenType,
+            final PwmSetting smsToSend,
+            final Map<String, String> inputTokenData,
+            final MacroMachine macroMachine
     )
             throws PwmUnrecoverableException
     {
         final Configuration config = pwmRequest.getConfig();
-        final UserIdentity userIdentity = userInfo.getUserIdentity();
+        final UserIdentity userIdentity = userInfo == null ? null : userInfo.getUserIdentity();
         final Map<String, String> tokenMapData = new LinkedHashMap<>();
 
+        if ( userInfo != null )
         {
             final Instant userLastPasswordChange = userInfo.getPasswordLastModifiedTime();
             if ( userLastPasswordChange != null )
@@ -163,18 +242,12 @@ public class TokenUtil
             }
         }
 
-        final EmailItemBean emailItemBean = config.readSettingAsEmail( emailToSend, pwmRequest.getLocale() );
-        final MacroMachine.StringReplacer stringReplacer = ( matchedMacro, newValue ) ->
+        if ( inputTokenData != null )
         {
-            if ( "@User:Email@".equals( matchedMacro )  )
-            {
-                return tokenDestinationItem.getValue();
-            }
-
-            return newValue;
-        };
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity, stringReplacer );
+            tokenMapData.putAll( inputTokenData );
+        }
 
+        final EmailItemBean emailItemBean = config.readSettingAsEmail( emailToSend, pwmRequest.getLocale() );
         final String tokenKey;
         final TokenPayload tokenPayload;
         try
@@ -184,7 +257,7 @@ public class TokenUtil
                     new TimeDuration( config.readSettingAsLong( PwmSetting.TOKEN_LIFETIME ), TimeUnit.SECONDS ),
                     tokenMapData,
                     userIdentity,
-                    Collections.singleton( tokenDestinationItem.getValue() )
+                    tokenDestinationItem.getValue()
             );
             tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
         }
@@ -211,4 +284,16 @@ public class TokenUtil
         );
     }
 
+    public static MacroMachine.StringReplacer makeTokenDestStringReplacer( final TokenDestinationItem tokenDestinationItem )
+    {
+        return ( matchedMacro, newValue ) ->
+        {
+            if ( "@User:Email@".equals( matchedMacro )  )
+            {
+                return tokenDestinationItem.getValue();
+            }
+
+            return newValue;
+        };
+    }
 }

+ 28 - 0
server/src/main/java/password/pwm/util/form/FormUtility.java

@@ -30,9 +30,11 @@ import com.novell.ldapchai.util.SearchHelper;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.bean.SessionLabel;
+import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmDataValidationException;
@@ -596,4 +598,30 @@ public class FormUtility
         }
         return returnMap;
     }
+
+    public static Map<String, TokenDestinationItem.Type> identifyFormItemsNeedingPotentialTokenValidation(
+            final LdapProfile ldapProfile,
+            final Collection<FormConfiguration> formConfigurations
+    )
+    {
+        final Map<PwmSetting, TokenDestinationItem.Type> settingTypeMap = TokenDestinationItem.getSettingToDestTypeMap();
+        final Map<String, TokenDestinationItem.Type> returnObj = new LinkedHashMap<>(  );
+
+        for ( final Map.Entry<PwmSetting, TokenDestinationItem.Type> entry : settingTypeMap.entrySet() )
+        {
+            final String attrName = ldapProfile.readSettingAsString( entry.getKey() );
+            if ( !StringUtil.isEmpty( attrName ) )
+            {
+                for ( final FormConfiguration formConfiguration : formConfigurations )
+                {
+                    if ( attrName.equalsIgnoreCase( formConfiguration.getName() ) )
+                    {
+                        returnObj.put( attrName, entry.getValue() );
+                    }
+                }
+            }
+        }
+
+        return returnObj;
+    }
 }

+ 14 - 0
server/src/main/java/password/pwm/util/java/StringUtil.java

@@ -216,6 +216,20 @@ public abstract class StringUtil
         return PwmNumberFormat.forDefaultLocale().format( diskSize ) + " bytes";
     }
 
+    public static boolean nullSafeEqualsIgnoreCase( final String value1, final String value2 )
+    {
+        return value1 == null
+                ? value2 == null
+                : value1.equalsIgnoreCase( value2 );
+    }
+
+    public static boolean nullSafeEquals( final String value1, final String value2 )
+    {
+        return value1 == null
+                ? value2 == null
+                : value1.equals( value2 );
+    }
+
     public enum Base64Options
     {
         GZIP,

+ 11 - 0
server/src/main/java/password/pwm/util/macro/MacroMachine.java

@@ -330,6 +330,17 @@ public class MacroMachine
         return new MacroMachine( pwmApplication, sessionLabel, userInfo, loginInfoBean, null );
     }
 
+    public static MacroMachine forUser(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserInfo userInfo,
+            final LoginInfoBean loginInfoBean,
+            final StringReplacer stringReplacer
+    )
+    {
+        return new MacroMachine( pwmApplication, sessionLabel, userInfo, loginInfoBean, stringReplacer );
+    }
+
     public static MacroMachine forUser(
             final PwmApplication pwmApplication,
             final Locale userLocale,

+ 11 - 11
server/src/main/java/password/pwm/ws/server/rest/RestProfileServer.java

@@ -29,7 +29,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.option.WebServiceUsage;
 import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.ProfileUtility;
-import password.pwm.config.profile.UpdateAttributesProfile;
+import password.pwm.config.profile.UpdateProfileProfile;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
@@ -39,7 +39,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpContentType;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmHttpRequestWrapper;
-import password.pwm.http.servlet.UpdateProfileServlet;
+import password.pwm.http.servlet.updateprofile.UpdateProfileUtil;
 import password.pwm.i18n.Message;
 import password.pwm.ldap.LdapPermissionTester;
 import password.pwm.ldap.UserInfo;
@@ -132,16 +132,16 @@ public class RestProfileServer extends RestServlet
             throw new PwmUnrecoverableException( PwmError.ERROR_NO_PROFILE_ASSIGNED );
         }
 
-        final UpdateAttributesProfile updateAttributesProfile = restRequest.getPwmApplication().getConfig().getUpdateAttributesProfile().get( updateProfileID );
+        final UpdateProfileProfile updateProfileProfile = restRequest.getPwmApplication().getConfig().getUpdateAttributesProfile().get( updateProfileID );
 
         final Map<String, String> profileData = new HashMap<>();
         {
             final Map<FormConfiguration, String> formData = new HashMap<>();
-            for ( final FormConfiguration formConfiguration : updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM ) )
+            for ( final FormConfiguration formConfiguration : updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM ) )
             {
                 formData.put( formConfiguration, "" );
             }
-            final List<FormConfiguration> formFields = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+            final List<FormConfiguration> formFields = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
 
             final UserInfo userInfo = UserInfoFactory.newUserInfo(
                     restRequest.getPwmApplication(),
@@ -161,7 +161,7 @@ public class RestProfileServer extends RestServlet
 
         final JsonProfileData outputData = new JsonProfileData();
         outputData.profile = profileData;
-        outputData.formDefinition = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        outputData.formDefinition = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
         final RestResultBean restResultBean = RestResultBean.withData( outputData );
         StatisticsManager.incrementStat( restRequest.getPwmApplication(), Statistic.REST_PROFILE );
         return restResultBean;
@@ -205,10 +205,10 @@ public class RestProfileServer extends RestServlet
             throw new PwmUnrecoverableException( PwmError.ERROR_NO_PROFILE_ASSIGNED );
         }
 
-        final UpdateAttributesProfile updateAttributesProfile = restRequest.getPwmApplication().getConfig().getUpdateAttributesProfile().get( updateProfileID );
+        final UpdateProfileProfile updateProfileProfile = restRequest.getPwmApplication().getConfig().getUpdateAttributesProfile().get( updateProfileID );
 
         {
-            final List<UserPermission> userPermission = updateAttributesProfile.readSettingAsUserPermission( PwmSetting.UPDATE_PROFILE_QUERY_MATCH );
+            final List<UserPermission> userPermission = updateProfileProfile.readSettingAsUserPermission( PwmSetting.UPDATE_PROFILE_QUERY_MATCH );
             final boolean result = LdapPermissionTester.testUserPermissions(
                     restRequest.getPwmApplication(),
                     restRequest.getSessionLabel(),
@@ -224,7 +224,7 @@ public class RestProfileServer extends RestServlet
 
 
         final FormMap inputFormData = new FormMap( jsonInput.profile );
-        final List<FormConfiguration> profileForm = updateAttributesProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
+        final List<FormConfiguration> profileForm = updateProfileProfile.readSettingAsForm( PwmSetting.UPDATE_PROFILE_FORM );
         final Map<FormConfiguration, String> profileFormData = new HashMap<>();
         for ( final FormConfiguration formConfiguration : profileForm )
         {
@@ -249,13 +249,13 @@ public class RestProfileServer extends RestServlet
                 targetUserIdentity.getUserIdentity()
         );
 
-        UpdateProfileServlet.doProfileUpdate(
+        UpdateProfileUtil.doProfileUpdate(
                 restRequest.getPwmApplication(),
                 restRequest.getSessionLabel(),
                 restRequest.getLocale(),
                 userInfo,
                 macroMachine,
-                updateAttributesProfile,
+                updateProfileProfile,
                 FormUtility.asStringMap( profileFormData ),
                 targetUserIdentity.getChaiUser()
         );

+ 0 - 1
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -3018,7 +3018,6 @@
             <option value="email">email</option>
             <option value="number">number</option>
             <option value="password">password</option>
-            <option value="random">random</option>
             <option value="tel">tel</option>
             <option value="hidden">hidden</option>
             <!--<option value="date">date</option>-->

+ 1 - 6
server/src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp

@@ -122,12 +122,7 @@
                     Destination(s)
                 </td>
                 <td>
-                    <%for (final Iterator destIter = tokenPayload.getDest().iterator(); destIter.hasNext();) { %>
-                    <%=destIter.next()%>
-                    <% if (destIter.hasNext()) { %>
-                    <br/>
-                    <% } %>
-                    <% } %>
+                    <%=JspUtility.freindlyWrite(pageContext, tokenPayload.getDestination())%>
                 </td>
             </tr>
             <tr>

+ 1 - 0
server/src/main/webapp/WEB-INF/jsp/configeditor.jsp

@@ -156,6 +156,7 @@
 <pwm:script-ref url="/public/resources/js/uilibrary.js"/>
 <pwm:script-ref url="/public/resources/js/configeditor.js"/>
 <pwm:script-ref url="/public/resources/js/configeditor-settings.js"/>
+<pwm:script-ref url="/public/resources/js/configeditor-settings-form.js"/>
 <pwm:script-ref url="/public/resources/js/configeditor-settings-challenges.js"/>
 <pwm:script-ref url="/public/resources/js/configeditor-settings-customlink.js"/>
 <pwm:script-ref url="/public/resources/js/configeditor-settings-remotewebservices.js"/>

+ 6 - 11
server/src/main/webapp/WEB-INF/jsp/newuser-entercode.jsp

@@ -20,15 +20,9 @@
  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 --%>
 
-<%@ page import="password.pwm.bean.TokenVerificationProgress" %>
-<%@ page import="password.pwm.http.bean.NewUserBean" %>
+<%@ page import="password.pwm.bean.TokenDestinationItem" %>
 <%@ page import="password.pwm.http.servlet.newuser.NewUserServlet" %>
-<%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 
-<%
-    final NewUserBean newUserBean = JspUtility.getSessionBean(pageContext,NewUserBean.class);
-    final TokenVerificationProgress tokenVerificationProgress = newUserBean.getTokenVerificationProgress();
-%>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -40,11 +34,12 @@
         <jsp:param name="pwm.PageName" value="Title_NewUser"/>
     </jsp:include>
     <div id="centerbody">
+        <% TokenDestinationItem tokenDestinationItem = (TokenDestinationItem )JspUtility.getAttribute(pageContext, PwmRequestAttribute.TokenDestItems);%>
         <h1 id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></h1>
-        <% if (newUserBean.getTokenVerificationProgress().getPhase() == TokenVerificationProgress.TokenChannel.EMAIL) { %>
-        <p><pwm:display key="Display_RecoverEnterCode" value1="<%=tokenVerificationProgress.getTokenDisplayText()%>"/></p>
-        <% } else if (newUserBean.getTokenVerificationProgress().getPhase() == TokenVerificationProgress.TokenChannel.SMS) { %>
-        <p><pwm:display key="Display_RecoverEnterCodeSMS" value1="<%=tokenVerificationProgress.getTokenDisplayText()%>"/></p>
+        <% if (tokenDestinationItem.getType() == TokenDestinationItem.Type.email) { %>
+        <p><pwm:display key="Display_RecoverEnterCode" value1="<%=tokenDestinationItem.getDisplay()%>"/></p>
+        <% } else if (tokenDestinationItem.getType() == TokenDestinationItem.Type.sms) { %>
+        <p><pwm:display key="Display_RecoverEnterCodeSMS" value1="<%=tokenDestinationItem.getDisplay()%>"/></p>
         <% } %>
         <form action="<pwm:current-url/>" method="post" autocomplete="off" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form">
             <%@ include file="fragment/message.jsp" %>

+ 7 - 23
server/src/main/webapp/WEB-INF/jsp/updateprofile-entercode.jsp

@@ -20,17 +20,13 @@
  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 --%>
 
-<%@ page import="password.pwm.bean.TokenVerificationProgress" %>
 <%@ page import="password.pwm.http.bean.NewUserBean" %>
 <%@ page import="password.pwm.http.servlet.newuser.NewUserServlet" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ page import="password.pwm.http.bean.UpdateProfileBean" %>
-<%@ page import="password.pwm.config.profile.UpdateAttributesProfile" %>
+<%@ page import="password.pwm.config.profile.UpdateProfileProfile" %>
+<%@ page import="password.pwm.bean.TokenDestinationItem" %>
 
-<%
-    final UpdateProfileBean updateProfileBean = JspUtility.getSessionBean(pageContext,UpdateProfileBean.class);
-    final TokenVerificationProgress tokenVerificationProgress = updateProfileBean.getTokenVerificationProgress();
-%>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -42,11 +38,12 @@
         <jsp:param name="pwm.PageName" value="Title_UpdateProfile"/>
     </jsp:include>
     <div id="centerbody">
+        <% TokenDestinationItem tokenDestinationItem = (TokenDestinationItem )JspUtility.getAttribute(pageContext, PwmRequestAttribute.TokenDestItems);%>
         <h1 id="page-content-title"><pwm:display key="Title_UpdateProfile" displayIfMissing="true"/></h1>
-        <% if (updateProfileBean.getTokenVerificationProgress().getPhase() == TokenVerificationProgress.TokenChannel.EMAIL) { %>
-        <p><pwm:display key="Display_UpdateProfileEnterCode" value1="<%=tokenVerificationProgress.getTokenDisplayText()%>"/></p>
-        <% } else if (updateProfileBean.getTokenVerificationProgress().getPhase() == TokenVerificationProgress.TokenChannel.SMS) { %>
-        <p><pwm:display key="Display_UpdateProfileEnterCodeSMS" value1="<%=tokenVerificationProgress.getTokenDisplayText()%>"/></p>
+        <% if (tokenDestinationItem.getType() == TokenDestinationItem.Type.email) { %>
+        <p><pwm:display key="Display_UpdateProfileEnterCode" value1="<%=tokenDestinationItem.getDisplay()%>"/></p>
+        <% } else if (tokenDestinationItem.getType() == TokenDestinationItem.Type.sms) { %>
+        <p><pwm:display key="Display_UpdateProfileEnterCodeSMS" value1="<%=tokenDestinationItem.getDisplay()%>"/></p>
         <% } %>
         <form action="<pwm:current-url/>" method="post" autocomplete="off" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form">
             <%@ include file="fragment/message.jsp" %>
@@ -73,19 +70,6 @@
     </div>
     <div class="push"></div>
 </div>
-<pwm:script> <%-- ie doesn't support 'from' attribute on buttons, so handle with this script --%>
-    <script type="text/javascript">
-        PWM_GLOBAL['startupFunctions'].push(function() {
-            PWM_MAIN.addEventHandler('button-reset', 'click', function(e){
-                console.log('intercepted cancel button');
-                PWM_MAIN.cancelEvent(e);
-                var cancelForm = PWM_MAIN.getObject('form-reset');
-                PWM_MAIN.handleFormSubmit(cancelForm);
-            });
-        });
-    </script>
-</pwm:script>
-
 <%@ include file="fragment/footer.jsp" %>
 </body>
 </html>

+ 551 - 0
server/src/main/webapp/public/resources/js/configeditor-settings-form.js

@@ -0,0 +1,551 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+var FormTableHandler = {};
+FormTableHandler.newRowValue = {
+    name:'',
+    minimumLength:0,
+    maximumLength:255,
+    labels:{'':''},
+    regexErrors:{'':''},
+    selectOptions:{},
+    description:{'':''}
+};
+
+FormTableHandler.init = function(keyName) {
+    console.log('FormTableHandler init for ' + keyName);
+    var parentDiv = 'table_setting_' + keyName;
+    PWM_CFGEDIT.clearDivElements(parentDiv, true);
+    PWM_CFGEDIT.readSetting(keyName, function(resultValue) {
+        PWM_VAR['clientSettingCache'][keyName] = resultValue;
+        FormTableHandler.redraw(keyName);
+    });
+};
+
+FormTableHandler.redraw = function(keyName) {
+    var resultValue = PWM_VAR['clientSettingCache'][keyName];
+    var parentDiv = 'table_setting_' + keyName;
+    var parentDivElement = PWM_MAIN.getObject(parentDiv);
+
+    parentDivElement.innerHTML = '<table class="noborder" style="margin-left: 0; width:auto" id="table-top-' + keyName + '"></table>';
+    parentDiv = 'table-top-' + keyName;
+    parentDivElement = PWM_MAIN.getObject(parentDiv);
+
+    if (!PWM_MAIN.JSLibrary.isEmpty(resultValue)) {
+        var headerRow = document.createElement("tr");
+        var rowHtml = '<td>Name</td><td></td><td>Label</td>';
+        headerRow.innerHTML = rowHtml;
+        parentDivElement.appendChild(headerRow);
+    }
+
+    for (var i in resultValue) {
+        FormTableHandler.drawRow(parentDiv, keyName, i, resultValue[i]);
+    }
+
+    var buttonRow = document.createElement("tr");
+    buttonRow.setAttribute("colspan","5");
+    buttonRow.innerHTML = '<td><button class="btn" id="button-' + keyName + '-addRow"><span class="btn-icon pwm-icon pwm-icon-plus-square"></span>Add Item</button></td>';
+
+    parentDivElement.appendChild(buttonRow);
+
+    PWM_MAIN.addEventHandler('button-' + keyName + '-addRow','click',function(){
+        FormTableHandler.addRow(keyName);
+    });
+
+};
+
+FormTableHandler.drawRow = function(parentDiv, settingKey, iteration, value) {
+    require(["dojo/json"], function(JSON){
+        var itemCount = PWM_MAIN.JSLibrary.itemCount(PWM_VAR['clientSettingCache'][settingKey]);
+        var inputID = 'value_' + settingKey + '_' + iteration + "_";
+        var options = PWM_SETTINGS['settings'][settingKey]['options'];
+        var properties = PWM_SETTINGS['settings'][settingKey]['properties'];
+
+        var newTableRow = document.createElement("tr");
+        newTableRow.setAttribute("style", "border-width: 0");
+
+        var htmlRow = '';
+        htmlRow += '<td style="background: #f6f9f8; border:1px solid #dae1e1; width:180px"><div class="noWrapTextBox" id="panel-name-' + inputID + '" ></div></td>';
+        htmlRow += '<td style="width:1px" id="icon-editLabel-' + inputID + '"><span class="btn-icon pwm-icon pwm-icon-edit"></span></td>';
+        htmlRow += '<td style="background: #f6f9f8; border:1px solid #dae1e1; width:170px"><div style="" class="noWrapTextBox " id="' + inputID + 'label"><span>' + value['labels'][''] + '</span></div></td>';
+
+        var userDNtypeAllowed = options['type-userDN'] === 'show';
+        if (!PWM_MAIN.JSLibrary.isEmpty(options)) {
+            htmlRow += '<td style="width:15px;">';
+            htmlRow += '<select id="' + inputID + 'type">';
+            for (var optionItem in options) {
+                //if (optionList[optionItem] !== 'userDN' || userDNtypeAllowed) {
+                var optionName = options[optionItem];
+                var selected = (optionName === PWM_VAR['clientSettingCache'][settingKey][iteration]['type']);
+                htmlRow += '<option value="' + optionName + '"' + (selected ? " selected" : "") + '>' + optionName + '</option>';
+                //}
+            }
+            htmlRow += '</select>';
+            htmlRow += '</td>';
+        }
+
+        var hideOptions = PWM_MAIN.JSLibrary.arrayContains(PWM_SETTINGS['settings'][settingKey]['flags'], 'Form_HideOptions');
+        if (!hideOptions) {
+            htmlRow += '<td class="noborder" style="min-width:90px;"><button id="' + inputID + 'optionsButton"><span class="btn-icon pwm-icon pwm-icon-sliders"/> Options</button></td>';
+        }
+
+        htmlRow += '<td style="width:10px">';
+        if (itemCount > 1 && iteration !== (itemCount -1)) {
+            htmlRow += '<span id="' + inputID + '-moveDown" class="action-icon pwm-icon pwm-icon-chevron-down"></span>';
+        }
+        htmlRow += '</td>';
+
+        htmlRow += '<td style="width:10px">';
+        if (itemCount > 1 && iteration !== 0) {
+            htmlRow += '<span id="' + inputID + '-moveUp" class="action-icon pwm-icon pwm-icon-chevron-up"></span>';
+        }
+        htmlRow += '</td>';
+        htmlRow += '<td style="width:10px"><span class="delete-row-icon action-icon pwm-icon pwm-icon-times" id="' + inputID + '-deleteRowButton"></span></td>';
+
+        newTableRow.innerHTML = htmlRow;
+        var parentDivElement = PWM_MAIN.getObject(parentDiv);
+        parentDivElement.appendChild(newTableRow);
+
+        UILibrary.addTextValueToElement("panel-name-" + inputID,value['name']);
+
+        PWM_MAIN.addEventHandler(inputID + "-moveUp", 'click', function () {
+            FormTableHandler.move(settingKey, true, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "-moveDown", 'click', function () {
+            FormTableHandler.move(settingKey, false, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "-deleteRowButton", 'click', function () {
+            FormTableHandler.removeRow(settingKey, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "label", 'click, keypress', function () {
+            FormTableHandler.showLabelDialog(settingKey, iteration);
+        });
+        PWM_MAIN.addEventHandler("icon-editLabel-" + inputID, 'click, keypress', function () {
+            FormTableHandler.showLabelDialog(settingKey, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "optionsButton", 'click', function () {
+            FormTableHandler.showOptionsDialog(settingKey, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "name", 'input', function () {
+            PWM_VAR['clientSettingCache'][settingKey][iteration]['name'] = PWM_MAIN.getObject(inputID + "name").value;
+            FormTableHandler.write(settingKey);
+        });
+        PWM_MAIN.addEventHandler(inputID + "type", 'click', function () {
+            PWM_VAR['clientSettingCache'][settingKey][iteration]['type'] = PWM_MAIN.getObject(inputID + "type").value;
+            FormTableHandler.write(settingKey);
+        });
+    });
+};
+
+FormTableHandler.write = function(settingKey, finishFunction) {
+    var cachedSetting = PWM_VAR['clientSettingCache'][settingKey];
+    PWM_CFGEDIT.writeSetting(settingKey, cachedSetting, finishFunction);
+};
+
+FormTableHandler.removeRow = function(keyName, iteration) {
+    PWM_MAIN.showConfirmDialog({
+        text:'Are you sure you wish to delete this item?',
+        okAction:function(){
+            var currentValues = PWM_VAR['clientSettingCache'][keyName];
+            currentValues.splice(iteration,1);
+            FormTableHandler.write(keyName,function(){
+                FormTableHandler.init(keyName);
+            });
+        }
+    });
+};
+
+FormTableHandler.move = function(settingKey, moveUp, iteration) {
+    var currentValues = PWM_VAR['clientSettingCache'][settingKey];
+    if (moveUp) {
+        FormTableHandler.arrayMoveUtil(currentValues, iteration, iteration - 1);
+    } else {
+        FormTableHandler.arrayMoveUtil(currentValues, iteration, iteration + 1);
+    }
+    FormTableHandler.write(settingKey);
+    FormTableHandler.redraw(settingKey);
+};
+
+FormTableHandler.arrayMoveUtil = function(arr, fromIndex, toIndex) {
+    var element = arr[fromIndex];
+    arr.splice(fromIndex, 1);
+    arr.splice(toIndex, 0, element);
+};
+
+
+FormTableHandler.addRow = function(keyName) {
+    UILibrary.stringEditorDialog({
+        title:PWM_SETTINGS['settings'][keyName]['label'] + ' - New Form Field',
+        regex:'^[a-zA-Z][a-zA-Z0-9-]*$',
+        placeholder:'FieldName',
+        completeFunction:function(value){
+            for (var i in PWM_VAR['clientSettingCache'][keyName]) {
+                if (PWM_VAR['clientSettingCache'][keyName][i]['name'] === value) {
+                    alert('field already exists');
+                    return;
+                }
+            }
+            var currentSize = PWM_MAIN.JSLibrary.itemCount(PWM_VAR['clientSettingCache'][keyName]);
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1] = FormTableHandler.newRowValue;
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].name = value;
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].labels = {'':value};
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].type = 'text';
+            FormTableHandler.write(keyName,function(){
+                FormTableHandler.init(keyName);
+            });
+        }
+    });
+};
+
+FormTableHandler.showOptionsDialog = function(keyName, iteration) {
+    var type = PWM_VAR['clientSettingCache'][keyName][iteration]['type'];
+    var settings = PWM_SETTINGS['settings'][keyName];
+    var options = 'options' in PWM_SETTINGS['settings'][keyName] ? PWM_SETTINGS['settings'][keyName]['options'] : {};
+    var currentValue = PWM_VAR['clientSettingCache'][keyName][iteration];
+
+    var hideStandardOptions = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_HideStandardOptions');
+    var showRequired = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowRequiredOption') && (type !== 'checkbox');
+    var showUnique = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowUniqueOption');
+    var showReadOnly = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowReadOnlyOption');
+    var showMultiValue = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowMultiValueOption');
+    var showConfirmation = type !== 'checkbox' && type !== 'select' && !hideStandardOptions;
+    var showSource = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowSource');
+
+
+    var inputID = 'value_' + keyName + '_' + iteration + "_";
+    var bodyText = '<div style="max-height: 500px; overflow-y: auto"><table class="noborder">';
+    if (!hideStandardOptions) {
+        bodyText += '<tr>';
+        var descriptionValue = currentValue['description'][''];
+        bodyText += '<td id="' + inputID + '-label-description" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Description') + '">Description</td><td>';
+        bodyText += '<div class="noWrapTextBox" id="' + inputID + 'description"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + descriptionValue + '...</span></div>';
+        bodyText += '</td>';
+    }
+
+    bodyText += '</tr><tr>';
+    if (showRequired) {
+        bodyText += '<td id="' + inputID + '-label-required" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Required') + '">Required</td><td><input type="checkbox" id="' + inputID + 'required' + '"/></td>';
+        bodyText += '</tr><tr>';
+    }
+    if (showConfirmation) {
+        bodyText += '<td id="' + inputID + '-label-confirm" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Confirm') + '">Confirm</td><td><input type="checkbox" id="' + inputID + 'confirmationRequired' + '"/></td>';
+        bodyText += '</tr><tr>';
+    }
+    if (showReadOnly) {
+        bodyText += '<td id="' + inputID + '-label-readOnly" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_ReadOnly') + '">Read Only</td><td><input type="checkbox" id="' + inputID + 'readonly' + '"/></td>';
+        bodyText += '</tr><tr>';
+    }
+    if (showUnique) {
+        bodyText += '<td id="' + inputID + '-label-unique" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Unique') + '">Unique</td><td><input type="checkbox" id="' + inputID + 'unique' + '"/></td>';
+        bodyText += '</tr><tr>';
+    }
+    if (showMultiValue) {
+        bodyText += '<td id="' + inputID + '-label-multivalue" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_MultiValue') + '">MultiValue</td><td><input type="checkbox" id="' + inputID + 'multivalue' + '"/></td>';
+        bodyText += '</tr><tr>';
+    }
+
+    if (!hideStandardOptions) {
+        bodyText += '<td class="key">Minimum Length</td><td><input min="0" pattern="[0-9]{1,5}" required max="65536" style="width: 70px" type="number" id="' + inputID + 'minimumLength' + '"/></td>';
+        bodyText += '</tr><tr>';
+        bodyText += '<td class="key">Maximum Length</td><td><input min="0" pattern="[0-9]{1,5}" max="65536" style="width: 70px" type="number" id="' + inputID + 'maximumLength' + '"/></td>';
+        bodyText += '</tr><tr>';
+
+        { // regex
+            bodyText += '<td id="' + inputID + '-label-regex" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Regex') + '">Regular Expression</td><td><input type="text" class="configStringInput" style="width:300px" id="' + inputID + 'regex' + '"/></td>';
+            bodyText += '</tr><tr>';
+
+            var regexErrorValue = currentValue['regexErrors'][''];
+            bodyText += '<td id="' + inputID + '-label-regexError" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_RegexError') + '">Regular Expression<br/>Error Message</td><td>';
+            bodyText += '<div class="noWrapTextBox" id="' + inputID + 'regexErrors"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + regexErrorValue + '...</span></div>';
+            bodyText += '</td>';
+            bodyText += '</tr><tr>';
+        }
+        bodyText += '<td id="' + inputID + '-label-placeholder" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Placeholder') + '">Placeholder</td><td><input type="text" class="configStringInput" style="width:300px" id="' + inputID + 'placeholder' + '"/></td>';
+        bodyText += '</tr><tr>';
+        bodyText += '<td id="' + inputID + '-label-js" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Javascript') + '">JavaScript (Depreciated)</td><td><input type="text" class="configStringInput" style="width:300px" id="' + inputID + 'javascript' + '"/></td>';
+        bodyText += '</tr><tr>';
+        if (currentValue['type'] === 'select') {
+            bodyText += '<td class="key">Select Options</td><td><button id="' + inputID + 'editOptionsButton"><span class="btn-icon pwm-icon pwm-icon-list-ul"/> Edit</button></td>';
+            bodyText += '</tr>';
+        }
+    }
+
+    if (showSource) {
+        bodyText += '<td id="' + inputID + '-label-source" class="key">Data Source</td>'
+            +  '<td><select id="' + inputID + 'source' + '">'
+            + '<option value="ldap">LDAP</option>'
+            + '<option value="remote">Remote REST API</option>'
+            + '</select></td></tr><tr>';
+    }
+
+    bodyText += '</table></div>';
+
+    var initFormElements = function() {
+        var currentValue = PWM_VAR['clientSettingCache'][keyName][iteration];
+
+        PWM_MAIN.addEventHandler(inputID + 'editOptionsButton', 'click', function(){
+            FormTableHandler.showSelectOptionsDialog(keyName,iteration);
+        });
+
+        PWM_MAIN.addEventHandler(inputID + 'description','click',function(){
+            FormTableHandler.showDescriptionDialog(keyName, iteration);
+        });
+
+        if (showRequired) {
+            PWM_MAIN.getObject(inputID + "required").checked = currentValue['required'];
+            PWM_MAIN.addEventHandler(inputID + "required", "change", function(){
+                currentValue['required'] = PWM_MAIN.getObject(inputID + "required").checked;
+                FormTableHandler.write(keyName)
+            });
+        }
+
+        if (PWM_MAIN.getObject(inputID + "confirmationRequired") != null) {
+            PWM_MAIN.getObject(inputID + "confirmationRequired").checked = currentValue['confirmationRequired'];
+            PWM_MAIN.addEventHandler(inputID + "confirmationRequired", "change", function () {
+                currentValue['confirmationRequired'] = PWM_MAIN.getObject(inputID + "confirmationRequired").checked;
+                FormTableHandler.write(keyName)
+            });
+        }
+
+        if (showReadOnly) {
+            PWM_MAIN.getObject(inputID + "readonly").checked = currentValue['readonly'];
+            PWM_MAIN.addEventHandler(inputID + "readonly", "change", function () {
+                currentValue['readonly'] = PWM_MAIN.getObject(inputID + "readonly").checked;
+                FormTableHandler.write(keyName)
+            });
+        }
+
+        if (showUnique) {
+            PWM_MAIN.getObject(inputID + "unique").checked = currentValue['unique'];
+            PWM_MAIN.addEventHandler(inputID + "unique", "change", function () {
+                currentValue['unique'] = PWM_MAIN.getObject(inputID + "unique").checked;
+                FormTableHandler.write(keyName)
+            });
+        }
+
+        if (showMultiValue) {
+            PWM_MAIN.getObject(inputID + "multivalue").checked = currentValue['multivalue'];
+            PWM_MAIN.addEventHandler(inputID + "multivalue", "change", function () {
+                currentValue['multivalue'] = PWM_MAIN.getObject(inputID + "multivalue").checked;
+                FormTableHandler.write(keyName)
+            });
+        }
+
+        if (!hideStandardOptions) {
+            PWM_MAIN.getObject(inputID + "minimumLength").value = currentValue['minimumLength'];
+            PWM_MAIN.addEventHandler(inputID + "minimumLength", "change", function(){
+                currentValue['minimumLength'] = PWM_MAIN.getObject(inputID + "minimumLength").value;
+                FormTableHandler.write(keyName)
+            });
+
+            PWM_MAIN.getObject(inputID + "maximumLength").value = currentValue['maximumLength'];
+            PWM_MAIN.addEventHandler(inputID + "maximumLength", "change", function(){
+                currentValue['maximumLength'] = PWM_MAIN.getObject(inputID + "maximumLength").value;
+                FormTableHandler.write(keyName)
+            });
+
+            PWM_MAIN.getObject(inputID + "regex").value = currentValue['regex'];
+            PWM_MAIN.addEventHandler(inputID + "regex", "change", function(){
+                currentValue['regex'] = PWM_MAIN.getObject(inputID + "regex").value;
+                FormTableHandler.write(keyName)
+            });
+
+            PWM_MAIN.addEventHandler(inputID + 'regexErrors', 'click', function () {
+                FormTableHandler.showRegexErrorsDialog(keyName, iteration);
+            });
+
+            PWM_MAIN.getObject(inputID + "placeholder").value = currentValue['placeholder'];
+            PWM_MAIN.addEventHandler(inputID + "placeholder", "change", function(){
+                currentValue['placeholder'] = PWM_MAIN.getObject(inputID + "placeholder").value;
+                FormTableHandler.write(keyName)
+            });
+
+            PWM_MAIN.getObject(inputID + "javascript").value = currentValue['javascript'];
+            PWM_MAIN.addEventHandler(inputID + "javascript", "change", function(){
+                currentValue['javascript'] = PWM_MAIN.getObject(inputID + "javascript").value;
+                FormTableHandler.write(keyName)
+            });
+        }
+        if (showSource) {
+            var nodeID = inputID + 'source';
+            PWM_MAIN.JSLibrary.setValueOfSelectElement(nodeID,currentValue['source']);
+            PWM_MAIN.addEventHandler(nodeID,'change',function(){
+                var newValue = PWM_MAIN.JSLibrary.readValueOfSelectElement(nodeID);
+                currentValue['source'] = newValue;
+                FormTableHandler.write(keyName);
+            });
+        }
+    };
+
+    PWM_MAIN.showDialog({
+        title: PWM_SETTINGS['settings'][keyName]['label'] + ' - ' + currentValue['name'],
+        text:bodyText,
+        allowMove:true,
+        loadFunction:initFormElements,
+        okAction:function(){
+            FormTableHandler.redraw(keyName);
+        }
+    });
+};
+
+FormTableHandler.showLabelDialog = function(keyName, iteration) {
+    var finishAction = function(){ FormTableHandler.redraw(keyName); };
+    var title = 'Label for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
+    FormTableHandler.multiLocaleStringDialog(keyName, iteration, 'labels', finishAction, title);
+};
+
+FormTableHandler.multiLocaleStringDialog = function(keyName, iteration, settingType, finishAction, titleText) {
+    require(["dijit/Dialog","dijit/form/Textarea","dijit/form/CheckBox"],function(){
+        var inputID = 'value_' + keyName + '_' + iteration + "_" + "label_";
+        var bodyText = '<table class="noborder" id="' + inputID + 'table">';
+        bodyText += '<tr>';
+        for (var localeName in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
+            var value = PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName];
+            var localeID = inputID + localeName;
+            bodyText += '<td>' + localeName + '</td>';
+            bodyText += '<td><input style="width:420px" class="configStringInput" type="text" value="' + value + '" id="' + localeID + '-input"></input></td>';
+            if (localeName !== '') {
+                bodyText += '<td><span class="delete-row-icon action-icon pwm-icon pwm-icon-times" id="' + localeID + '-removeLocaleButton"></span></td>';
+            }
+            bodyText += '</tr><tr>';
+        }
+        bodyText += '</tr></table>';
+
+        PWM_MAIN.showDialog({
+            title: titleText,
+            text: bodyText,
+            okAction:function(){
+                finishAction();
+            },
+            loadFunction:function(){
+                for (var iter in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
+                    (function(localeName) {
+                        var localeID = inputID + localeName;
+                        PWM_MAIN.addEventHandler(localeID + '-input', 'input', function () {
+                            var inputElement = PWM_MAIN.getObject(localeID + '-input');
+                            var value = inputElement.value;
+                            PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName] = value;
+                            FormTableHandler.write(keyName);
+                        });
+                        PWM_MAIN.addEventHandler(localeID + '-removeLocaleButton', 'click', function () {
+                            delete PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName];
+                            FormTableHandler.write(keyName);
+                            FormTableHandler.multiLocaleStringDialog(keyName, iteration, settingType, finishAction, titleText);
+                        });
+                    }(iter));
+                }
+                UILibrary.addAddLocaleButtonRow(inputID + 'table', inputID, function(localeName){
+                    if (localeName in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
+                        alert('Locale is already present');
+                    } else {
+                        PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName] = '';
+                        FormTableHandler.write(keyName);
+                        FormTableHandler.multiLocaleStringDialog(keyName, iteration, settingType, finishAction, titleText);
+                    }
+                }, Object.keys(PWM_VAR['clientSettingCache'][keyName][iteration][settingType]));
+            }
+        });
+    });
+};
+
+
+FormTableHandler.showRegexErrorsDialog = function(keyName, iteration) {
+    var finishAction = function(){ FormTableHandler.showOptionsDialog(keyName, iteration); };
+    var title = 'Regular Expression Error Message for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
+    FormTableHandler.multiLocaleStringDialog(keyName, iteration, 'regexErrors', finishAction, title);
+};
+
+
+FormTableHandler.showSelectOptionsDialog = function(keyName, iteration) {
+    var inputID = 'value_' + keyName + '_' + iteration + "_" + "selectOptions_";
+    var bodyText = '';
+    bodyText += '<table class="noborder" id="' + inputID + 'table"">';
+    bodyText += '<tr>';
+    bodyText += '<td><b>Value</b></td><td><b>Display Name</b></td>';
+    bodyText += '</tr><tr>';
+    for (var optionName in PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions']) {
+        var value = PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName];
+        var optionID = inputID + optionName;
+        bodyText += '<td>' + optionName + '</td><td>' + value + '</td>';
+        bodyText += '<td class="noborder" style="width:15px">';
+        bodyText += '<span id="' + optionID + '-removeButton" class="delete-row-icon action-icon pwm-icon pwm-icon-times"></span>';
+        bodyText += '</td>';
+        bodyText += '</tr><tr>';
+    }
+    bodyText += '</tr></table>';
+    bodyText += '<br/><br/><br/>';
+    bodyText += '<input class="configStringInput" style="width:200px" type="text" placeholder="Value" required id="addSelectOptionName"/>';
+    bodyText += '<input class="configStringInput" style="width:200px" type="text" placeholder="Display Name" required id="addSelectOptionValue"/>';
+    bodyText += '<button id="addSelectOptionButton"><span class="btn-icon pwm-icon pwm-icon-plus-square"/> Add</button>';
+
+    PWM_MAIN.showDialog({
+        title: 'Select Options for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'],
+        text: bodyText,
+        okAction: function(){
+            FormTableHandler.showOptionsDialog(keyName,iteration);
+        }
+    });
+
+    for (var optionName in PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions']) {
+        var loopID = inputID + optionName;
+        var optionID = inputID + optionName;
+        PWM_MAIN.clearDijitWidget(loopID);
+        PWM_MAIN.addEventHandler(optionID + '-removeButton','click',function(){
+            FormTableHandler.removeSelectOptionsOption(keyName,iteration,optionName);
+        });
+    }
+
+    PWM_MAIN.addEventHandler('addSelectOptionButton','click',function(){
+        var value = PWM_MAIN.getObject('addSelectOptionName').value;
+        var display = PWM_MAIN.getObject('addSelectOptionValue').value;
+        FormTableHandler.addSelectOptionsOption(keyName, iteration, value, display);
+    });
+};
+
+FormTableHandler.addSelectOptionsOption = function(keyName, iteration, optionName, optionValue) {
+    if (optionName === null || optionName.length < 1) {
+        alert('Name field is required');
+        return;
+    }
+
+    if (optionValue === null || optionValue.length < 1) {
+        alert('Value field is required');
+        return;
+    }
+
+    PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName] = optionValue;
+    FormTableHandler.write(keyName);
+    FormTableHandler.showSelectOptionsDialog(keyName, iteration);
+};
+
+FormTableHandler.removeSelectOptionsOption = function(keyName, iteration, optionName) {
+    delete PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName];
+    FormTableHandler.write(keyName);
+    FormTableHandler.showSelectOptionsDialog(keyName, iteration);
+};
+
+FormTableHandler.showDescriptionDialog = function(keyName, iteration) {
+    var finishAction = function(){ FormTableHandler.showOptionsDialog(keyName, iteration); };
+    var title = 'Description for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
+    FormTableHandler.multiLocaleStringDialog(keyName, iteration, 'description', finishAction, title);
+};

+ 0 - 529
server/src/main/webapp/public/resources/js/configeditor-settings.js

@@ -600,535 +600,6 @@ MultiLocaleTableHandler.writeMultiLocaleSetting = function(settingKey, locale, i
     MultiLocaleTableHandler.draw(settingKey);
 };
 
-// -------------------------- form table handler ------------------------------------
-
-var FormTableHandler = {};
-FormTableHandler.newRowValue = {
-    name:'',
-    minimumLength:0,
-    maximumLength:255,
-    labels:{'':''},
-    regexErrors:{'':''},
-    selectOptions:{},
-    description:{'':''}
-};
-
-FormTableHandler.init = function(keyName) {
-    console.log('FormTableHandler init for ' + keyName);
-    var parentDiv = 'table_setting_' + keyName;
-    PWM_CFGEDIT.clearDivElements(parentDiv, true);
-    PWM_CFGEDIT.readSetting(keyName, function(resultValue) {
-        PWM_VAR['clientSettingCache'][keyName] = resultValue;
-        FormTableHandler.redraw(keyName);
-    });
-};
-
-FormTableHandler.redraw = function(keyName) {
-    var resultValue = PWM_VAR['clientSettingCache'][keyName];
-    var parentDiv = 'table_setting_' + keyName;
-    var parentDivElement = PWM_MAIN.getObject(parentDiv);
-
-    parentDivElement.innerHTML = '<table class="noborder" style="margin-left: 0; width:auto" id="table-top-' + keyName + '"></table>';
-    parentDiv = 'table-top-' + keyName;
-    parentDivElement = PWM_MAIN.getObject(parentDiv);
-
-    if (!PWM_MAIN.JSLibrary.isEmpty(resultValue)) {
-        var headerRow = document.createElement("tr");
-        var rowHtml = '<td>Name</td><td></td><td>Label</td>';
-        headerRow.innerHTML = rowHtml;
-        parentDivElement.appendChild(headerRow);
-    }
-
-    for (var i in resultValue) {
-        FormTableHandler.drawRow(parentDiv, keyName, i, resultValue[i]);
-    }
-
-    var buttonRow = document.createElement("tr");
-    buttonRow.setAttribute("colspan","5");
-    buttonRow.innerHTML = '<td><button class="btn" id="button-' + keyName + '-addRow"><span class="btn-icon pwm-icon pwm-icon-plus-square"></span>Add Item</button></td>';
-
-    parentDivElement.appendChild(buttonRow);
-
-    PWM_MAIN.addEventHandler('button-' + keyName + '-addRow','click',function(){
-        FormTableHandler.addRow(keyName);
-    });
-
-};
-
-FormTableHandler.drawRow = function(parentDiv, settingKey, iteration, value) {
-    require(["dojo/json"], function(JSON){
-        var itemCount = PWM_MAIN.JSLibrary.itemCount(PWM_VAR['clientSettingCache'][settingKey]);
-        var inputID = 'value_' + settingKey + '_' + iteration + "_";
-        var options = PWM_SETTINGS['settings'][settingKey]['options'];
-        var properties = PWM_SETTINGS['settings'][settingKey]['properties'];
-
-        var newTableRow = document.createElement("tr");
-        newTableRow.setAttribute("style", "border-width: 0");
-
-        var htmlRow = '';
-        htmlRow += '<td style="background: #f6f9f8; border:1px solid #dae1e1; width:180px"><div class="noWrapTextBox" id="panel-name-' + inputID + '" ></div></td>';
-        htmlRow += '<td style="width:1px" id="icon-editLabel-' + inputID + '"><span class="btn-icon pwm-icon pwm-icon-edit"></span></td>';
-        htmlRow += '<td style="background: #f6f9f8; border:1px solid #dae1e1; width:170px"><div style="" class="noWrapTextBox " id="' + inputID + 'label"><span>' + value['labels'][''] + '</span></div></td>';
-
-        var userDNtypeAllowed = options['type-userDN'] === 'show';
-        if (!PWM_MAIN.JSLibrary.isEmpty(options)) {
-            htmlRow += '<td style="width:15px;">';
-            htmlRow += '<select id="' + inputID + 'type">';
-            for (var optionItem in options) {
-                //if (optionList[optionItem] !== 'userDN' || userDNtypeAllowed) {
-                var optionName = options[optionItem];
-                var selected = (optionName === PWM_VAR['clientSettingCache'][settingKey][iteration]['type']);
-                htmlRow += '<option value="' + optionName + '"' + (selected ? " selected" : "") + '>' + optionName + '</option>';
-                //}
-            }
-            htmlRow += '</select>';
-            htmlRow += '</td>';
-        }
-
-        var hideOptions = PWM_MAIN.JSLibrary.arrayContains(PWM_SETTINGS['settings'][settingKey]['flags'], 'Form_HideOptions');
-        if (!hideOptions) {
-            htmlRow += '<td class="noborder" style="min-width:90px;"><button id="' + inputID + 'optionsButton"><span class="btn-icon pwm-icon pwm-icon-sliders"/> Options</button></td>';
-        }
-
-        htmlRow += '<td style="width:10px">';
-        if (itemCount > 1 && iteration !== (itemCount -1)) {
-            htmlRow += '<span id="' + inputID + '-moveDown" class="action-icon pwm-icon pwm-icon-chevron-down"></span>';
-        }
-        htmlRow += '</td>';
-
-        htmlRow += '<td style="width:10px">';
-        if (itemCount > 1 && iteration !== 0) {
-            htmlRow += '<span id="' + inputID + '-moveUp" class="action-icon pwm-icon pwm-icon-chevron-up"></span>';
-        }
-        htmlRow += '</td>';
-        htmlRow += '<td style="width:10px"><span class="delete-row-icon action-icon pwm-icon pwm-icon-times" id="' + inputID + '-deleteRowButton"></span></td>';
-
-        newTableRow.innerHTML = htmlRow;
-        var parentDivElement = PWM_MAIN.getObject(parentDiv);
-        parentDivElement.appendChild(newTableRow);
-
-        UILibrary.addTextValueToElement("panel-name-" + inputID,value['name']);
-
-        PWM_MAIN.addEventHandler(inputID + "-moveUp", 'click', function () {
-            FormTableHandler.move(settingKey, true, iteration);
-        });
-        PWM_MAIN.addEventHandler(inputID + "-moveDown", 'click', function () {
-            FormTableHandler.move(settingKey, false, iteration);
-        });
-        PWM_MAIN.addEventHandler(inputID + "-deleteRowButton", 'click', function () {
-            FormTableHandler.removeRow(settingKey, iteration);
-        });
-        PWM_MAIN.addEventHandler(inputID + "label", 'click, keypress', function () {
-            FormTableHandler.showLabelDialog(settingKey, iteration);
-        });
-        PWM_MAIN.addEventHandler("icon-editLabel-" + inputID, 'click, keypress', function () {
-            FormTableHandler.showLabelDialog(settingKey, iteration);
-        });
-        PWM_MAIN.addEventHandler(inputID + "optionsButton", 'click', function () {
-            FormTableHandler.showOptionsDialog(settingKey, iteration);
-        });
-        PWM_MAIN.addEventHandler(inputID + "name", 'input', function () {
-            PWM_VAR['clientSettingCache'][settingKey][iteration]['name'] = PWM_MAIN.getObject(inputID + "name").value;
-            FormTableHandler.write(settingKey);
-        });
-        PWM_MAIN.addEventHandler(inputID + "type", 'click', function () {
-            PWM_VAR['clientSettingCache'][settingKey][iteration]['type'] = PWM_MAIN.getObject(inputID + "type").value;
-            FormTableHandler.write(settingKey);
-        });
-    });
-};
-
-FormTableHandler.write = function(settingKey, finishFunction) {
-    var cachedSetting = PWM_VAR['clientSettingCache'][settingKey];
-    PWM_CFGEDIT.writeSetting(settingKey, cachedSetting, finishFunction);
-};
-
-FormTableHandler.removeRow = function(keyName, iteration) {
-    PWM_MAIN.showConfirmDialog({
-        text:'Are you sure you wish to delete this item?',
-        okAction:function(){
-            var currentValues = PWM_VAR['clientSettingCache'][keyName];
-            currentValues.splice(iteration,1);
-            FormTableHandler.write(keyName,function(){
-                FormTableHandler.init(keyName);
-            });
-        }
-    });
-};
-
-FormTableHandler.move = function(settingKey, moveUp, iteration) {
-    var currentValues = PWM_VAR['clientSettingCache'][settingKey];
-    if (moveUp) {
-        FormTableHandler.arrayMoveUtil(currentValues, iteration, iteration - 1);
-    } else {
-        FormTableHandler.arrayMoveUtil(currentValues, iteration, iteration + 1);
-    }
-    FormTableHandler.write(settingKey);
-    FormTableHandler.redraw(settingKey);
-};
-
-FormTableHandler.arrayMoveUtil = function(arr, fromIndex, toIndex) {
-    var element = arr[fromIndex];
-    arr.splice(fromIndex, 1);
-    arr.splice(toIndex, 0, element);
-};
-
-
-FormTableHandler.addRow = function(keyName) {
-    UILibrary.stringEditorDialog({
-        title:PWM_SETTINGS['settings'][keyName]['label'] + ' - New Form Field',
-        regex:'^[a-zA-Z][a-zA-Z0-9-]*$',
-        placeholder:'FieldName',
-        completeFunction:function(value){
-            for (var i in PWM_VAR['clientSettingCache'][keyName]) {
-                if (PWM_VAR['clientSettingCache'][keyName][i]['name'] === value) {
-                    alert('field already exists');
-                    return;
-                }
-            }
-            var currentSize = PWM_MAIN.JSLibrary.itemCount(PWM_VAR['clientSettingCache'][keyName]);
-            PWM_VAR['clientSettingCache'][keyName][currentSize + 1] = FormTableHandler.newRowValue;
-            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].name = value;
-            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].labels = {'':value};
-            FormTableHandler.write(keyName,function(){
-                FormTableHandler.init(keyName);
-            });
-        }
-    });
-};
-
-FormTableHandler.showOptionsDialog = function(keyName, iteration) {
-    var type = PWM_VAR['clientSettingCache'][keyName][iteration]['type'];
-    var settings = PWM_SETTINGS['settings'][keyName];
-    var options = 'options' in PWM_SETTINGS['settings'][keyName] ? PWM_SETTINGS['settings'][keyName]['options'] : {};
-    var currentValue = PWM_VAR['clientSettingCache'][keyName][iteration];
-
-    var hideStandardOptions = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_HideStandardOptions');
-    var showRequired = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowRequiredOption') && (type !== 'checkbox');
-    var showUnique = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowUniqueOption');
-    var showReadOnly = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowReadOnlyOption');
-    var showMultiValue = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowMultiValueOption');
-    var showConfirmation = type !== 'checkbox' && type !== 'select' && !hideStandardOptions;
-    var showSource = PWM_MAIN.JSLibrary.arrayContains(settings['flags'],'Form_ShowSource');
-
-
-    var inputID = 'value_' + keyName + '_' + iteration + "_";
-    var bodyText = '<div style="max-height: 500px; overflow-y: auto"><table class="noborder">';
-    if (!hideStandardOptions) {
-        bodyText += '<tr>';
-        var descriptionValue = currentValue['description'][''];
-        bodyText += '<td id="' + inputID + '-label-description" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Description') + '">Description</td><td>';
-        bodyText += '<div class="noWrapTextBox" id="' + inputID + 'description"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + descriptionValue + '...</span></div>';
-        bodyText += '</td>';
-    }
-
-    bodyText += '</tr><tr>';
-    if (showRequired) {
-        bodyText += '<td id="' + inputID + '-label-required" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Required') + '">Required</td><td><input type="checkbox" id="' + inputID + 'required' + '"/></td>';
-        bodyText += '</tr><tr>';
-    }
-    if (showConfirmation) {
-        bodyText += '<td id="' + inputID + '-label-confirm" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Confirm') + '">Confirm</td><td><input type="checkbox" id="' + inputID + 'confirmationRequired' + '"/></td>';
-        bodyText += '</tr><tr>';
-    }
-    if (showReadOnly) {
-        bodyText += '<td id="' + inputID + '-label-readOnly" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_ReadOnly') + '">Read Only</td><td><input type="checkbox" id="' + inputID + 'readonly' + '"/></td>';
-        bodyText += '</tr><tr>';
-    }
-    if (showUnique) {
-        bodyText += '<td id="' + inputID + '-label-unique" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Unique') + '">Unique</td><td><input type="checkbox" id="' + inputID + 'unique' + '"/></td>';
-        bodyText += '</tr><tr>';
-    }
-    if (showMultiValue) {
-        bodyText += '<td id="' + inputID + '-label-multivalue" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_MultiValue') + '">MultiValue</td><td><input type="checkbox" id="' + inputID + 'multivalue' + '"/></td>';
-        bodyText += '</tr><tr>';
-    }
-
-    if (!hideStandardOptions) {
-        bodyText += '<td class="key">Minimum Length</td><td><input min="0" pattern="[0-9]{1,5}" required max="65536" style="width: 70px" type="number" id="' + inputID + 'minimumLength' + '"/></td>';
-        bodyText += '</tr><tr>';
-        bodyText += '<td class="key">Maximum Length</td><td><input min="0" pattern="[0-9]{1,5}" max="65536" style="width: 70px" type="number" id="' + inputID + 'maximumLength' + '"/></td>';
-        bodyText += '</tr><tr>';
-
-        { // regex
-            bodyText += '<td id="' + inputID + '-label-regex" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Regex') + '">Regular Expression</td><td><input type="text" class="configStringInput" style="width:300px" id="' + inputID + 'regex' + '"/></td>';
-            bodyText += '</tr><tr>';
-
-            var regexErrorValue = currentValue['regexErrors'][''];
-            bodyText += '<td id="' + inputID + '-label-regexError" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_RegexError') + '">Regular Expression<br/>Error Message</td><td>';
-            bodyText += '<div class="noWrapTextBox" id="' + inputID + 'regexErrors"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + regexErrorValue + '...</span></div>';
-            bodyText += '</td>';
-            bodyText += '</tr><tr>';
-        }
-        bodyText += '<td id="' + inputID + '-label-placeholder" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Placeholder') + '">Placeholder</td><td><input type="text" class="configStringInput" style="width:300px" id="' + inputID + 'placeholder' + '"/></td>';
-        bodyText += '</tr><tr>';
-        bodyText += '<td id="' + inputID + '-label-js" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Javascript') + '">JavaScript (Depreciated)</td><td><input type="text" class="configStringInput" style="width:300px" id="' + inputID + 'javascript' + '"/></td>';
-        bodyText += '</tr><tr>';
-        if (currentValue['type'] === 'select') {
-            bodyText += '<td class="key">Select Options</td><td><button id="' + inputID + 'editOptionsButton"><span class="btn-icon pwm-icon pwm-icon-list-ul"/> Edit</button></td>';
-            bodyText += '</tr>';
-        }
-    }
-
-    if (showSource) {
-        bodyText += '<td id="' + inputID + '-label-source" class="key">Data Source</td>'
-            +  '<td><select id="' + inputID + 'source' + '">'
-            + '<option value="ldap">LDAP</option>'
-            + '<option value="remote">Remote REST API</option>'
-            + '</select></td></tr><tr>';
-    }
-
-    bodyText += '</table></div>';
-
-    var initFormElements = function() {
-        var currentValue = PWM_VAR['clientSettingCache'][keyName][iteration];
-
-        PWM_MAIN.addEventHandler(inputID + 'editOptionsButton', 'click', function(){
-            FormTableHandler.showSelectOptionsDialog(keyName,iteration);
-        });
-
-        PWM_MAIN.addEventHandler(inputID + 'description','click',function(){
-            FormTableHandler.showDescriptionDialog(keyName, iteration);
-        });
-
-        if (showRequired) {
-            PWM_MAIN.getObject(inputID + "required").checked = currentValue['required'];
-            PWM_MAIN.addEventHandler(inputID + "required", "change", function(){
-                currentValue['required'] = PWM_MAIN.getObject(inputID + "required").checked;
-                FormTableHandler.write(keyName)
-            });
-        }
-
-        if (PWM_MAIN.getObject(inputID + "confirmationRequired") != null) {
-            PWM_MAIN.getObject(inputID + "confirmationRequired").checked = currentValue['confirmationRequired'];
-            PWM_MAIN.addEventHandler(inputID + "confirmationRequired", "change", function () {
-                currentValue['confirmationRequired'] = PWM_MAIN.getObject(inputID + "confirmationRequired").checked;
-                FormTableHandler.write(keyName)
-            });
-        }
-
-        if (showReadOnly) {
-            PWM_MAIN.getObject(inputID + "readonly").checked = currentValue['readonly'];
-            PWM_MAIN.addEventHandler(inputID + "readonly", "change", function () {
-                currentValue['readonly'] = PWM_MAIN.getObject(inputID + "readonly").checked;
-                FormTableHandler.write(keyName)
-            });
-        }
-
-        if (showUnique) {
-            PWM_MAIN.getObject(inputID + "unique").checked = currentValue['unique'];
-            PWM_MAIN.addEventHandler(inputID + "unique", "change", function () {
-                currentValue['unique'] = PWM_MAIN.getObject(inputID + "unique").checked;
-                FormTableHandler.write(keyName)
-            });
-        }
-
-        if (showMultiValue) {
-            PWM_MAIN.getObject(inputID + "multivalue").checked = currentValue['multivalue'];
-            PWM_MAIN.addEventHandler(inputID + "multivalue", "change", function () {
-                currentValue['multivalue'] = PWM_MAIN.getObject(inputID + "multivalue").checked;
-                FormTableHandler.write(keyName)
-            });
-        }
-
-        if (!hideStandardOptions) {
-            PWM_MAIN.getObject(inputID + "minimumLength").value = currentValue['minimumLength'];
-            PWM_MAIN.addEventHandler(inputID + "minimumLength", "change", function(){
-                currentValue['minimumLength'] = PWM_MAIN.getObject(inputID + "minimumLength").value;
-                FormTableHandler.write(keyName)
-            });
-
-            PWM_MAIN.getObject(inputID + "maximumLength").value = currentValue['maximumLength'];
-            PWM_MAIN.addEventHandler(inputID + "maximumLength", "change", function(){
-                currentValue['maximumLength'] = PWM_MAIN.getObject(inputID + "maximumLength").value;
-                FormTableHandler.write(keyName)
-            });
-
-            PWM_MAIN.getObject(inputID + "regex").value = currentValue['regex'];
-            PWM_MAIN.addEventHandler(inputID + "regex", "change", function(){
-                currentValue['regex'] = PWM_MAIN.getObject(inputID + "regex").value;
-                FormTableHandler.write(keyName)
-            });
-
-            PWM_MAIN.addEventHandler(inputID + 'regexErrors', 'click', function () {
-                FormTableHandler.showRegexErrorsDialog(keyName, iteration);
-            });
-
-            PWM_MAIN.getObject(inputID + "placeholder").value = currentValue['placeholder'];
-            PWM_MAIN.addEventHandler(inputID + "placeholder", "change", function(){
-                currentValue['placeholder'] = PWM_MAIN.getObject(inputID + "placeholder").value;
-                FormTableHandler.write(keyName)
-            });
-
-            PWM_MAIN.getObject(inputID + "javascript").value = currentValue['javascript'];
-            PWM_MAIN.addEventHandler(inputID + "javascript", "change", function(){
-                currentValue['javascript'] = PWM_MAIN.getObject(inputID + "javascript").value;
-                FormTableHandler.write(keyName)
-            });
-        }
-        if (showSource) {
-            var nodeID = inputID + 'source';
-            PWM_MAIN.JSLibrary.setValueOfSelectElement(nodeID,currentValue['source']);
-            PWM_MAIN.addEventHandler(nodeID,'change',function(){
-                var newValue = PWM_MAIN.JSLibrary.readValueOfSelectElement(nodeID);
-                currentValue['source'] = newValue;
-                FormTableHandler.write(keyName);
-            });
-        }
-    };
-
-    PWM_MAIN.showDialog({
-        title: PWM_SETTINGS['settings'][keyName]['label'] + ' - ' + currentValue['name'],
-        text:bodyText,
-        allowMove:true,
-        loadFunction:initFormElements,
-        okAction:function(){
-            FormTableHandler.redraw(keyName);
-        }
-    });
-};
-
-FormTableHandler.showLabelDialog = function(keyName, iteration) {
-    var finishAction = function(){ FormTableHandler.redraw(keyName); };
-    var title = 'Label for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
-    FormTableHandler.multiLocaleStringDialog(keyName, iteration, 'labels', finishAction, title);
-};
-
-FormTableHandler.multiLocaleStringDialog = function(keyName, iteration, settingType, finishAction, titleText) {
-    require(["dijit/Dialog","dijit/form/Textarea","dijit/form/CheckBox"],function(){
-        var inputID = 'value_' + keyName + '_' + iteration + "_" + "label_";
-        var bodyText = '<table class="noborder" id="' + inputID + 'table">';
-        bodyText += '<tr>';
-        for (var localeName in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
-            var value = PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName];
-            var localeID = inputID + localeName;
-            bodyText += '<td>' + localeName + '</td>';
-            bodyText += '<td><input style="width:420px" class="configStringInput" type="text" value="' + value + '" id="' + localeID + '-input"></input></td>';
-            if (localeName !== '') {
-                bodyText += '<td><span class="delete-row-icon action-icon pwm-icon pwm-icon-times" id="' + localeID + '-removeLocaleButton"></span></td>';
-            }
-            bodyText += '</tr><tr>';
-        }
-        bodyText += '</tr></table>';
-
-        PWM_MAIN.showDialog({
-            title: titleText,
-            text: bodyText,
-            okAction:function(){
-                finishAction();
-            },
-            loadFunction:function(){
-                for (var iter in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
-                    (function(localeName) {
-                        var localeID = inputID + localeName;
-                        PWM_MAIN.addEventHandler(localeID + '-input', 'input', function () {
-                            var inputElement = PWM_MAIN.getObject(localeID + '-input');
-                            var value = inputElement.value;
-                            PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName] = value;
-                            FormTableHandler.write(keyName);
-                        });
-                        PWM_MAIN.addEventHandler(localeID + '-removeLocaleButton', 'click', function () {
-                            delete PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName];
-                            FormTableHandler.write(keyName);
-                            FormTableHandler.multiLocaleStringDialog(keyName, iteration, settingType, finishAction, titleText);
-                        });
-                    }(iter));
-                }
-                UILibrary.addAddLocaleButtonRow(inputID + 'table', inputID, function(localeName){
-                    if (localeName in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
-                        alert('Locale is already present');
-                    } else {
-                        PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName] = '';
-                        FormTableHandler.write(keyName);
-                        FormTableHandler.multiLocaleStringDialog(keyName, iteration, settingType, finishAction, titleText);
-                    }
-                }, Object.keys(PWM_VAR['clientSettingCache'][keyName][iteration][settingType]));
-            }
-        });
-    });
-};
-
-
-FormTableHandler.showRegexErrorsDialog = function(keyName, iteration) {
-    var finishAction = function(){ FormTableHandler.showOptionsDialog(keyName, iteration); };
-    var title = 'Regular Expression Error Message for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
-    FormTableHandler.multiLocaleStringDialog(keyName, iteration, 'regexErrors', finishAction, title);
-};
-
-
-FormTableHandler.showSelectOptionsDialog = function(keyName, iteration) {
-    var inputID = 'value_' + keyName + '_' + iteration + "_" + "selectOptions_";
-    var bodyText = '';
-    bodyText += '<table class="noborder" id="' + inputID + 'table"">';
-    bodyText += '<tr>';
-    bodyText += '<td><b>Value</b></td><td><b>Display Name</b></td>';
-    bodyText += '</tr><tr>';
-    for (var optionName in PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions']) {
-        var value = PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName];
-        var optionID = inputID + optionName;
-        bodyText += '<td>' + optionName + '</td><td>' + value + '</td>';
-        bodyText += '<td class="noborder" style="width:15px">';
-        bodyText += '<span id="' + optionID + '-removeButton" class="delete-row-icon action-icon pwm-icon pwm-icon-times"></span>';
-        bodyText += '</td>';
-        bodyText += '</tr><tr>';
-    }
-    bodyText += '</tr></table>';
-    bodyText += '<br/><br/><br/>';
-    bodyText += '<input class="configStringInput" style="width:200px" type="text" placeholder="Value" required id="addSelectOptionName"/>';
-    bodyText += '<input class="configStringInput" style="width:200px" type="text" placeholder="Display Name" required id="addSelectOptionValue"/>';
-    bodyText += '<button id="addSelectOptionButton"><span class="btn-icon pwm-icon pwm-icon-plus-square"/> Add</button>';
-
-    PWM_MAIN.showDialog({
-        title: 'Select Options for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'],
-        text: bodyText,
-        okAction: function(){
-            FormTableHandler.showOptionsDialog(keyName,iteration);
-        }
-    });
-
-    for (var optionName in PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions']) {
-        var loopID = inputID + optionName;
-        var optionID = inputID + optionName;
-        PWM_MAIN.clearDijitWidget(loopID);
-        PWM_MAIN.addEventHandler(optionID + '-removeButton','click',function(){
-            FormTableHandler.removeSelectOptionsOption(keyName,iteration,optionName);
-        });
-    }
-
-    PWM_MAIN.addEventHandler('addSelectOptionButton','click',function(){
-        var value = PWM_MAIN.getObject('addSelectOptionName').value;
-        var display = PWM_MAIN.getObject('addSelectOptionValue').value;
-        FormTableHandler.addSelectOptionsOption(keyName, iteration, value, display);
-    });
-};
-
-FormTableHandler.addSelectOptionsOption = function(keyName, iteration, optionName, optionValue) {
-    if (optionName === null || optionName.length < 1) {
-        alert('Name field is required');
-        return;
-    }
-
-    if (optionValue === null || optionValue.length < 1) {
-        alert('Value field is required');
-        return;
-    }
-
-    PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName] = optionValue;
-    FormTableHandler.write(keyName);
-    FormTableHandler.showSelectOptionsDialog(keyName, iteration);
-};
-
-FormTableHandler.removeSelectOptionsOption = function(keyName, iteration, optionName) {
-    delete PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName];
-    FormTableHandler.write(keyName);
-    FormTableHandler.showSelectOptionsDialog(keyName, iteration);
-};
-
-FormTableHandler.showDescriptionDialog = function(keyName, iteration) {
-    var finishAction = function(){ FormTableHandler.showOptionsDialog(keyName, iteration); };
-    var title = 'Description for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
-    FormTableHandler.multiLocaleStringDialog(keyName, iteration, 'description', finishAction, title);
-};
 
 
 // -------------------------- change password handler ------------------------------------