瀏覽代碼

Merge remote-tracking branch 'origin/master' into ng-helpdesk

jalbr74 7 年之前
父節點
當前提交
826050d74d
共有 47 個文件被更改,包括 3229 次插入3025 次删除
  1. 2 2
      server/pom.xml
  2. 25 4
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  3. 0 71
      server/src/main/java/password/pwm/bean/TokenVerificationProgress.java
  4. 5 5
      server/src/main/java/password/pwm/config/Configuration.java
  5. 2 0
      server/src/main/java/password/pwm/config/PwmSetting.java
  6. 4 4
      server/src/main/java/password/pwm/config/profile/UpdateProfileProfile.java
  7. 1 0
      server/src/main/java/password/pwm/http/JspUrl.java
  8. 4 1
      server/src/main/java/password/pwm/http/PwmRequestAttribute.java
  9. 3 3
      server/src/main/java/password/pwm/http/SessionManager.java
  10. 16 70
      server/src/main/java/password/pwm/http/bean/ActivateUserBean.java
  11. 10 7
      server/src/main/java/password/pwm/http/bean/NewUserBean.java
  12. 8 8
      server/src/main/java/password/pwm/http/bean/UpdateProfileBean.java
  13. 0 879
      server/src/main/java/password/pwm/http/servlet/ActivateUserServlet.java
  14. 4 2
      server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java
  15. 0 880
      server/src/main/java/password/pwm/http/servlet/UpdateProfileServlet.java
  16. 487 0
      server/src/main/java/password/pwm/http/servlet/activation/ActivateUserServlet.java
  17. 390 0
      server/src/main/java/password/pwm/http/servlet/activation/ActivateUserUtils.java
  18. 27 18
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  19. 16 136
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  20. 64 80
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java
  21. 155 184
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  22. 453 0
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileServlet.java
  23. 407 0
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java
  24. 6 6
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  25. 16 3
      server/src/main/java/password/pwm/svc/token/TokenDestinationDisplayMasker.java
  26. 4 4
      server/src/main/java/password/pwm/svc/token/TokenPayload.java
  27. 16 14
      server/src/main/java/password/pwm/svc/token/TokenService.java
  28. 2 4
      server/src/main/java/password/pwm/svc/token/TokenType.java
  29. 299 0
      server/src/main/java/password/pwm/svc/token/TokenUtil.java
  30. 28 0
      server/src/main/java/password/pwm/util/form/FormUtility.java
  31. 14 0
      server/src/main/java/password/pwm/util/java/StringUtil.java
  32. 11 0
      server/src/main/java/password/pwm/util/macro/MacroMachine.java
  33. 13 32
      server/src/main/java/password/pwm/ws/client/rest/RestTokenDataClient.java
  34. 11 11
      server/src/main/java/password/pwm/ws/server/rest/RestProfileServer.java
  35. 6 1
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  36. 12 13
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  37. 1 2
      server/src/main/webapp/WEB-INF/jsp/activateuser-agreement.jsp
  38. 21 9
      server/src/main/webapp/WEB-INF/jsp/activateuser-entercode.jsp
  39. 117 0
      server/src/main/webapp/WEB-INF/jsp/activateuser-tokenchoice.jsp
  40. 1 6
      server/src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp
  41. 1 0
      server/src/main/webapp/WEB-INF/jsp/configeditor.jsp
  42. 2 2
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-tokenchoice.jsp
  43. 6 11
      server/src/main/webapp/WEB-INF/jsp/newuser-entercode.jsp
  44. 7 23
      server/src/main/webapp/WEB-INF/jsp/updateprofile-entercode.jsp
  45. 1 1
      server/src/main/webapp/public/resources/js/configeditor-settings-challenges.js
  46. 551 0
      server/src/main/webapp/public/resources/js/configeditor-settings-form.js
  47. 0 529
      server/src/main/webapp/public/resources/js/configeditor-settings.js

+ 2 - 2
server/pom.xml

@@ -738,12 +738,12 @@
         <dependency>
             <groupId>com.blueconic</groupId>
             <artifactId>browscap-java</artifactId>
-            <version>1.2.1</version>
+            <version>1.2.2</version>
         </dependency>
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.2.0</version>
+            <version>1.2.1</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>

+ 25 - 4
server/src/main/java/password/pwm/bean/TokenDestinationItem.java

@@ -26,16 +26,18 @@ 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;
-import password.pwm.util.ValueObfuscator;
+import password.pwm.svc.token.TokenDestinationDisplayMasker;
 import password.pwm.util.java.StringUtil;
 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;
@@ -76,7 +97,7 @@ public class TokenDestinationItem implements Serializable
         final Configuration configuration = pwmApplication.getConfig();
         final SecureService secureService = pwmApplication.getSecureService();
 
-        final ValueObfuscator valueObfuscator = new ValueObfuscator( configuration );
+        final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( configuration );
 
         final Map<String, TokenDestinationItem> results = new LinkedHashMap<>(  );
 
@@ -93,7 +114,7 @@ public class TokenDestinationItem implements Serializable
                 final String idHash = secureService.hash( emailValue + Type.email.name() );
                 final TokenDestinationItem item = TokenDestinationItem.builder()
                         .id( idHash )
-                        .display( valueObfuscator.maskEmail( emailValue ) )
+                        .display( tokenDestinationDisplayMasker.maskEmail( emailValue ) )
                         .value( emailValue )
                         .type( Type.email )
                         .build();
@@ -114,7 +135,7 @@ public class TokenDestinationItem implements Serializable
                 final String idHash = secureService.hash( smsValue + Type.sms.name() );
                 final TokenDestinationItem item = TokenDestinationItem.builder()
                         .id( idHash )
-                        .display( valueObfuscator.maskPhone( smsValue ) )
+                        .display( tokenDestinationDisplayMasker.maskPhone( smsValue ) )
                         .value( smsValue )
                         .type( Type.sms )
                         .build();

+ 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:

+ 2 - 0
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -615,6 +615,8 @@ public enum PwmSetting
             "token.lifetime", PwmSettingSyntax.DURATION, PwmSettingCategory.TOKEN ),
     TOKEN_LDAP_ATTRIBUTE(
             "token.ldap.attribute", PwmSettingSyntax.STRING, PwmSettingCategory.TOKEN ),
+    TOKEN_ENABLE_VALUE_MASKING(
+            "token.valueMasking.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.TOKEN ),
 
     // OTP
     OTP_PROFILE_LIST(

+ 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 );
 
     }
 

+ 1 - 0
server/src/main/java/password/pwm/http/JspUrl.java

@@ -39,6 +39,7 @@ public enum JspUrl
     ADMIN_DEBUG( "admin-user-debug.jsp" ),
     ACTIVATE_USER( "activateuser.jsp" ),
     ACTIVATE_USER_AGREEMENT( "activateuser-agreement.jsp" ),
+    ACTIVATE_USER_TOKEN_CHOICE( "activateuser-tokenchoice.jsp" ),
     ACTIVATE_USER_ENTER_CODE( "activateuser-entercode.jsp" ),
     LOGIN( "login.jsp" ),
     LOGIN_PW_ONLY( "login-passwordonly.jsp" ),

+ 4 - 1
server/src/main/java/password/pwm/http/PwmRequestAttribute.java

@@ -79,7 +79,6 @@ public enum PwmRequestAttribute
     ForgottenPasswordInstructions,
     ForgottenPasswordOtpRecord,
     ForgottenPasswordResendTokenEnabled,
-    ForgottenPasswordTokenDestItems,
     ForgottenPasswordInhibitPasswordReset,
 
     GuestCurrentExpirationDate,
@@ -96,4 +95,8 @@ public enum PwmRequestAttribute
 
     UserDebugData,
     AppDashboardData,
+
+    TokenDestItems,
+
+    ShowGoBackButton,
 }

+ 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

+ 16 - 70
server/src/main/java/password/pwm/http/bean/ActivateUserBean.java

@@ -22,92 +22,38 @@
 
 package password.pwm.http.bean;
 
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.option.SessionBeanMode;
 
 import java.util.Collections;
 import java.util.Set;
 
+@Data
+@EqualsAndHashCode( callSuper = false )
 public class ActivateUserBean extends PwmSessionBean
 {
-    private boolean tokenIssued;
+    @SerializedName( "tp" )
     private boolean tokenPassed;
+
+    @SerializedName( "ap" )
     private boolean agreementPassed;
+
+    @SerializedName( "v" )
     private boolean formValidated;
-    private String tokenDisplayText;
-    private String agreementText;
 
+    @SerializedName( "u" )
     private UserIdentity userIdentity;
 
-    public boolean isTokenIssued( )
-    {
-        return tokenIssued;
-    }
+    @SerializedName( "ts" )
+    private boolean tokenSent;
 
-    public void setTokenIssued( final boolean tokenIssued )
-    {
-        this.tokenIssued = tokenIssued;
-    }
-
-    public boolean isAgreementPassed( )
-    {
-        return agreementPassed;
-    }
-
-    public void setAgreementPassed( final boolean agreementPassed )
-    {
-        this.agreementPassed = agreementPassed;
-    }
-
-    public boolean isFormValidated( )
-    {
-        return formValidated;
-    }
-
-    public void setFormValidated( final boolean formValidated )
-    {
-        this.formValidated = formValidated;
-    }
-
-    public boolean isTokenPassed( )
-    {
-        return tokenPassed;
-    }
+    @SerializedName( "td" )
+    private TokenDestinationItem tokenDestination;
 
-    public void setTokenPassed( final boolean tokenPassed )
-    {
-        this.tokenPassed = tokenPassed;
-    }
-
-    public UserIdentity getUserIdentity( )
-    {
-        return userIdentity;
-    }
-
-    public void setUserIdentity( final UserIdentity userIdentity )
-    {
-        this.userIdentity = userIdentity;
-    }
-
-    public String getTokenDisplayText( )
-    {
-        return tokenDisplayText;
-    }
-
-    public void setTokenDisplayText( final String tokenSendAddress )
-    {
-        this.tokenDisplayText = tokenSendAddress;
-    }
-
-    public String getAgreementText( )
-    {
-        return agreementText;
-    }
-
-    public void setAgreementText( final String agreementText )
-    {
-        this.agreementText = agreementText;
-    }
 
     public Type getType( )
     {

+ 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();
-    }
 }

+ 0 - 879
server/src/main/java/password/pwm/http/servlet/ActivateUserServlet.java

@@ -1,879 +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.ChaiOperationException;
-import com.novell.ldapchai.exception.ChaiUnavailableException;
-import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
-import com.novell.ldapchai.provider.ChaiProvider;
-import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
-import password.pwm.bean.EmailItemBean;
-import password.pwm.bean.LocalSessionStateBean;
-import password.pwm.bean.LoginInfoBean;
-import password.pwm.bean.TokenDestinationItem;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.option.MessageSendMethod;
-import password.pwm.config.profile.LdapProfile;
-import password.pwm.config.value.data.ActionConfiguration;
-import password.pwm.config.value.data.FormConfiguration;
-import password.pwm.config.value.data.UserPermission;
-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.HttpMethod;
-import password.pwm.http.JspUrl;
-import password.pwm.http.PwmRequest;
-import password.pwm.http.PwmSession;
-import password.pwm.http.bean.ActivateUserBean;
-import password.pwm.i18n.Message;
-import password.pwm.ldap.LdapPermissionTester;
-import password.pwm.ldap.UserInfo;
-import password.pwm.ldap.UserInfoFactory;
-import password.pwm.ldap.auth.AuthenticationType;
-import password.pwm.ldap.auth.PwmAuthenticationSource;
-import password.pwm.ldap.auth.SessionAuthenticator;
-import password.pwm.ldap.search.SearchConfiguration;
-import password.pwm.ldap.search.UserSearchEngine;
-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.CaptchaUtility;
-import password.pwm.util.PostChangePasswordAction;
-import password.pwm.util.form.FormUtility;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.TimeDuration;
-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 javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import java.io.IOException;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * User interaction servlet for creating new users (self registration).
- *
- * @author Jason D. Rivard
- */
-@WebServlet(
-        name = "ActivateUserServlet",
-        urlPatterns = {
-                PwmConstants.URL_PREFIX_PUBLIC + "/activate",
-                PwmConstants.URL_PREFIX_PUBLIC + "/activate/*",
-                PwmConstants.URL_PREFIX_PUBLIC + "/ActivateUser",
-                PwmConstants.URL_PREFIX_PUBLIC + "/ActivateUser/*",
-        }
-)
-public class ActivateUserServlet extends AbstractPwmServlet
-{
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( ActivateUserServlet.class );
-
-    public enum ActivateUserAction implements AbstractPwmServlet.ProcessAction
-    {
-        activate( HttpMethod.POST ),
-        enterCode( HttpMethod.POST, HttpMethod.GET ),
-        reset( HttpMethod.POST ),
-        agree( HttpMethod.POST ),;
-
-        private final Collection<HttpMethod> method;
-
-        ActivateUserAction( final HttpMethod... method )
-        {
-            this.method = Collections.unmodifiableList( Arrays.asList( method ) );
-        }
-
-        public Collection<HttpMethod> permittedMethods( )
-        {
-            return method;
-        }
-    }
-
-    protected ActivateUserAction readProcessAction( final PwmRequest request )
-            throws PwmUnrecoverableException
-    {
-        try
-        {
-            return ActivateUserAction.valueOf( request.readParameterAsString( PwmConstants.PARAM_ACTION_REQUEST ) );
-        }
-        catch ( IllegalArgumentException e )
-        {
-            return null;
-        }
-    }
-
-
-    protected void processAction( final PwmRequest pwmRequest )
-            throws ServletException, ChaiUnavailableException, IOException, PwmUnrecoverableException
-    {
-        //Fetch the session state bean.
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-
-        final Configuration config = pwmApplication.getConfig();
-
-        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-
-        if ( !config.readSettingAsBoolean( PwmSetting.ACTIVATE_USER_ENABLE ) )
-        {
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "activate user is not enabled" );
-            pwmRequest.respondWithError( errorInformation );
-            return;
-        }
-
-        if ( pwmSession.isAuthenticated() )
-        {
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_USERAUTHENTICATED );
-            pwmRequest.respondWithError( errorInformation );
-            return;
-        }
-
-        final ActivateUserAction action = readProcessAction( pwmRequest );
-
-        // convert a url command like /pwm/public/NewUserServlet/12321321 to redirect with a process action.
-        if ( action == null )
-        {
-            if ( pwmRequest.convertURLtokenCommand( PwmServletDefinition.ActivateUser, ActivateUserAction.enterCode ) )
-            {
-                return;
-            }
-        }
-        else
-        {
-            switch ( action )
-            {
-                case activate:
-                    handleActivationRequest( pwmRequest );
-                    break;
-
-                case enterCode:
-                    handleEnterTokenCode( pwmRequest );
-                    break;
-
-                case reset:
-                    pwmApplication.getSessionStateService().clearBean( pwmRequest, ActivateUserBean.class );
-                    forwardToActivateUserForm( pwmRequest );
-                    return;
-
-                case agree:
-                    handleAgreeRequest( pwmRequest, activateUserBean );
-                    advanceToNextStage( pwmRequest );
-                    break;
-
-                default:
-                    JavaHelper.unhandledSwitchStatement( action );
-
-            }
-        }
-
-        if ( !pwmRequest.getPwmResponse().isCommitted() )
-        {
-            this.advanceToNextStage( pwmRequest );
-        }
-    }
-
-    public void handleActivationRequest( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final LocalSessionStateBean ssBean = pwmSession.getSessionStateBean();
-
-        if ( CaptchaUtility.captchaEnabledForRequest( pwmRequest ) )
-        {
-            if ( !CaptchaUtility.verifyReCaptcha( pwmRequest ) )
-            {
-                final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_BAD_CAPTCHA_RESPONSE );
-                LOGGER.debug( pwmRequest, errorInfo );
-                setLastError( pwmRequest, errorInfo );
-                return;
-            }
-        }
-
-
-        pwmApplication.getSessionStateService().clearBean( pwmRequest, ActivateUserBean.class );
-        final List<FormConfiguration> configuredActivationForm = config.readSettingAsForm( PwmSetting.ACTIVATE_USER_FORM );
-
-        Map<FormConfiguration, String> formValues = new HashMap<>();
-        try
-        {
-            //read the values from the request
-            formValues = FormUtility.readFormValuesFromRequest( pwmRequest, configuredActivationForm,
-                    ssBean.getLocale() );
-
-            // check for intruders
-            pwmApplication.getIntruderManager().convenience().checkAttributes( formValues );
-
-            // read the context attr
-            final String contextParam = pwmRequest.readParameterAsString( PwmConstants.PARAM_CONTEXT );
-
-            // read the profile attr
-            final String ldapProfile = pwmRequest.readParameterAsString( PwmConstants.PARAM_LDAP_PROFILE );
-
-            // see if the values meet the configured form requirements.
-            FormUtility.validateFormValues( config, formValues, ssBean.getLocale() );
-
-            final String searchFilter = figureLdapSearchFilter( pwmRequest );
-
-            // read an ldap user object based on the params
-            final UserIdentity userIdentity;
-            {
-                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
-                final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
-                        .contexts( Collections.singletonList( contextParam ) )
-                        .filter( searchFilter )
-                        .formValues( formValues )
-                        .ldapProfile( ldapProfile )
-                        .build();
-
-                userIdentity = userSearchEngine.performSingleUserSearch( searchConfiguration, pwmRequest.getSessionLabel() );
-            }
-
-            validateParamsAgainstLDAP( pwmRequest, formValues, userIdentity );
-
-            final List<UserPermission> userPermissions = config.readSettingAsUserPermission( PwmSetting.ACTIVATE_USER_QUERY_MATCH );
-            if ( !LdapPermissionTester.testUserPermissions( pwmApplication, pwmSession.getLabel(), userIdentity, userPermissions ) )
-            {
-                final String errorMsg = "user " + userIdentity + " attempted activation, but does not match query string";
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATE_NO_PERMISSION, errorMsg );
-                pwmApplication.getIntruderManager().convenience().markUserIdentity( userIdentity, pwmSession );
-                pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-                throw new PwmUnrecoverableException( errorInformation );
-            }
-
-            final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-            activateUserBean.setUserIdentity( userIdentity );
-            activateUserBean.setFormValidated( true );
-            pwmApplication.getIntruderManager().convenience().clearAttributes( formValues );
-            pwmApplication.getIntruderManager().convenience().clearAddressAndSession( pwmSession );
-        }
-        catch ( PwmOperationalException e )
-        {
-            pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmSession );
-            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-            setLastError( pwmRequest, e.getErrorInformation() );
-            LOGGER.debug( pwmSession.getLabel(), e.getErrorInformation().toDebugStr() );
-        }
-
-        // redirect user to change password screen.
-        advanceToNextStage( pwmRequest );
-    }
-
-    private void handleAgreeRequest(
-            final PwmRequest pwmRequest,
-            final ActivateUserBean activateUserBean
-    )
-            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException
-    {
-        LOGGER.debug( pwmRequest, "user accepted agreement" );
-
-        if ( !activateUserBean.isAgreementPassed() )
-        {
-            activateUserBean.setAgreementPassed( true );
-            final AuditRecord auditRecord = new AuditRecordFactory( pwmRequest ).createUserAuditRecord(
-                    AuditEvent.AGREEMENT_PASSED,
-                    pwmRequest.getUserInfoIfLoggedIn(),
-                    pwmRequest.getSessionLabel(),
-                    "ActivateUser"
-            );
-            pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
-        }
-    }
-
-
-    private void advanceToNextStage( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-
-        if ( !activateUserBean.isFormValidated() || activateUserBean.getUserIdentity() == null )
-        {
-            forwardToActivateUserForm( pwmRequest );
-            return;
-        }
-
-        final boolean tokenRequired = MessageSendMethod.NONE != MessageSendMethod.valueOf( config.readSettingAsString( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD ) );
-        if ( tokenRequired )
-        {
-            if ( !activateUserBean.isTokenIssued() )
-            {
-                try
-                {
-                    final Locale locale = pwmSession.getSessionStateBean().getLocale();
-                    initializeToken( pwmRequest, locale, activateUserBean.getUserIdentity() );
-                }
-                catch ( PwmOperationalException e )
-                {
-                    setLastError( pwmRequest, e.getErrorInformation() );
-                    forwardToActivateUserForm( pwmRequest );
-                    return;
-                }
-            }
-
-            if ( !activateUserBean.isTokenPassed() )
-            {
-                pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_ENTER_CODE );
-                return;
-            }
-        }
-
-        final String agreementText = config.readSettingAsLocalizedString(
-                PwmSetting.ACTIVATE_AGREEMENT_MESSAGE,
-                pwmSession.getSessionStateBean().getLocale()
-        );
-        if ( agreementText != null && agreementText.length() > 0 && !activateUserBean.isAgreementPassed() )
-        {
-            if ( activateUserBean.getAgreementText() == null )
-            {
-                final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, activateUserBean.getUserIdentity() );
-                final String expandedText = macroMachine.expandMacros( agreementText );
-                activateUserBean.setAgreementText( expandedText );
-            }
-            pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_AGREEMENT );
-            return;
-        }
-
-        try
-        {
-            activateUser( pwmRequest, activateUserBean.getUserIdentity() );
-            pwmRequest.getPwmResponse().forwardToSuccessPage( Message.Success_ActivateUser );
-        }
-        catch ( PwmOperationalException e )
-        {
-            LOGGER.debug( pwmRequest, e.getErrorInformation() );
-            pwmApplication.getIntruderManager().convenience().markUserIdentity( activateUserBean.getUserIdentity(), pwmSession );
-            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-            pwmRequest.respondWithError( e.getErrorInformation() );
-        }
-    }
-
-    public void activateUser(
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity
-    )
-            throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
-        if ( config.readSettingAsBoolean( PwmSetting.ACTIVATE_USER_UNLOCK ) )
-        {
-            try
-            {
-                theUser.unlockPassword();
-            }
-            catch ( ChaiOperationException e )
-            {
-                final String errorMsg = "error unlocking user " + userIdentity + ": " + e.getMessage();
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
-                throw new PwmOperationalException( errorInformation );
-            }
-        }
-
-        try
-        {
-            {
-                // execute configured actions
-                LOGGER.debug( pwmSession.getLabel(), "executing configured pre-actions to user " + theUser.getEntryDN() );
-                final List<ActionConfiguration> configValues = config.readSettingAsAction( PwmSetting.ACTIVATE_USER_PRE_WRITE_ATTRIBUTES );
-                if ( configValues != null && !configValues.isEmpty() )
-                {
-                    final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
-
-                    final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
-                            .setExpandPwmMacros( true )
-                            .setMacroMachine( macroMachine )
-                            .createActionExecutor();
-
-                    actionExecutor.executeActions( configValues, pwmRequest.getSessionLabel() );
-                }
-            }
-
-            //authenticate the pwm session
-            final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator( pwmApplication, pwmSession, PwmAuthenticationSource.USER_ACTIVATION );
-            sessionAuthenticator.authUserWithUnknownPassword( userIdentity, AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
-
-            //ensure a change password is triggered
-            pwmSession.getLoginInfoBean().setType( AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
-            pwmSession.getLoginInfoBean().getAuthFlags().add( AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
-            pwmSession.getLoginInfoBean().getLoginFlags().add( LoginInfoBean.LoginFlag.forcePwChange );
-
-
-            // mark the event log
-            pwmApplication.getAuditManager().submit( AuditEvent.ACTIVATE_USER, pwmSession.getUserInfo(), pwmSession );
-
-            // update the stats bean
-            pwmApplication.getStatisticsManager().incrementValue( Statistic.ACTIVATED_USERS );
-
-            // send email or sms
-            sendPostActivationNotice( pwmRequest );
-
-            // setup post-change attributes
-            final PostChangePasswordAction postAction = new PostChangePasswordAction()
-            {
-
-                public String getLabel( )
-                {
-                    return "ActivateUser write attributes";
-                }
-
-                public boolean doAction( final PwmSession pwmSession, final String newPassword )
-                        throws PwmUnrecoverableException
-                {
-                    try
-                    {
-                        {
-                            // execute configured actions
-                            LOGGER.debug( pwmSession.getLabel(), "executing post-activate configured actions to user " + userIdentity.toDisplayString() );
-
-                            final MacroMachine macroMachine = pwmSession.getSessionManager().getMacroMachine( pwmApplication );
-                            final List<ActionConfiguration> configValues = pwmApplication.getConfig().readSettingAsAction( PwmSetting.ACTIVATE_USER_POST_WRITE_ATTRIBUTES );
-
-                            final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
-                                    .setExpandPwmMacros( true )
-                                    .setMacroMachine( macroMachine )
-                                    .createActionExecutor();
-                            actionExecutor.executeActions( configValues, pwmRequest.getSessionLabel() );
-                        }
-                    }
-                    catch ( PwmOperationalException e )
-                    {
-                        final ErrorInformation info = new ErrorInformation(
-                                PwmError.ERROR_ACTIVATION_FAILURE,
-                                e.getErrorInformation().getDetailedErrorMsg(), e.getErrorInformation().getFieldValues()
-                        );
-                        final PwmUnrecoverableException newException = new PwmUnrecoverableException( info );
-                        newException.initCause( e );
-                        throw newException;
-                    }
-                    catch ( ChaiUnavailableException e )
-                    {
-                        final String errorMsg = "unable to reach ldap server while writing post-activate attributes: " + e.getMessage();
-                        final ErrorInformation info = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
-                        final PwmUnrecoverableException newException = new PwmUnrecoverableException( info );
-                        newException.initCause( e );
-                        throw newException;
-                    }
-                    return true;
-                }
-            };
-
-            pwmSession.getUserSessionDataCacheBean().addPostChangePasswordActions( "activateUserWriteAttributes", postAction );
-        }
-        catch ( ImpossiblePasswordPolicyException e )
-        {
-            final ErrorInformation info = new ErrorInformation( PwmError.ERROR_UNKNOWN, "unexpected ImpossiblePasswordPolicyException error while activating user" );
-            LOGGER.warn( pwmSession, info, e );
-            throw new PwmOperationalException( info );
-        }
-    }
-
-    protected static void validateParamsAgainstLDAP(
-            final PwmRequest pwmRequest,
-            final Map<FormConfiguration, String> formValues,
-            final UserIdentity userIdentity
-    )
-            throws ChaiUnavailableException, PwmDataValidationException, PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final String searchFilter = figureLdapSearchFilter( pwmRequest );
-        final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider( userIdentity.getLdapProfileID() );
-        final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser( userIdentity.getUserDN() );
-
-        for ( final Map.Entry<FormConfiguration, String> entry : formValues.entrySet() )
-        {
-            final FormConfiguration formItem = entry.getKey();
-            final String attrName = formItem.getName();
-            final String tokenizedAttrName = "%" + attrName + "%";
-            if ( searchFilter.contains( tokenizedAttrName ) )
-            {
-                LOGGER.trace( pwmSession, "skipping validation of ldap value for '" + attrName + "' because it is in search filter" );
-            }
-            else
-            {
-                final String value = entry.getValue();
-                try
-                {
-                    if ( !chaiUser.compareStringAttribute( attrName, value ) )
-                    {
-                        final String errorMsg = "incorrect value for '" + attrName + "'";
-                        final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_ACTIVATION_VALIDATIONFAIL, errorMsg, new String[]
-                                {
-                                        attrName,
-                                }
-                        );
-                        LOGGER.debug( pwmSession.getLabel(), errorInfo.toDebugStr() );
-                        throw new PwmDataValidationException( errorInfo );
-                    }
-                    LOGGER.trace( pwmSession.getLabel(), "successful validation of ldap value for '" + attrName + "'" );
-                }
-                catch ( ChaiOperationException e )
-                {
-                    LOGGER.error( pwmSession.getLabel(), "error during param validation of '" + attrName + "', error: " + e.getMessage() );
-                    throw new PwmDataValidationException( new ErrorInformation(
-                            PwmError.ERROR_ACTIVATION_VALIDATIONFAIL,
-                            "ldap error testing value for '" + attrName + "'", new String[]
-                            {
-                                    attrName,
-                            }
-                    ) );
-                }
-            }
-        }
-    }
-
-    private void sendPostActivationNotice(
-            final PwmRequest pwmRequest
-    )
-            throws PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final UserInfo userInfo = pwmSession.getUserInfo();
-        final MessageSendMethod pref = MessageSendMethod.valueOf( config.readSettingAsString( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD ) );
-
-        final boolean success;
-        switch ( pref )
-        {
-            case SMSONLY:
-                // Only try SMS
-                success = sendPostActivationSms( pwmRequest );
-                break;
-            case EMAILONLY:
-            default:
-                // Only try email
-                success = sendPostActivationEmail( pwmRequest );
-                break;
-        }
-        if ( !success )
-        {
-            LOGGER.warn( pwmSession, "skipping send activation message for '" + userInfo.getUserIdentity() + "' no email or SMS number configured" );
-        }
-    }
-
-    private boolean sendPostActivationEmail(
-            final PwmRequest pwmRequest
-    )
-            throws PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final UserInfo userInfo = pwmSession.getUserInfo();
-        final Configuration config = pwmApplication.getConfig();
-        final Locale locale = pwmSession.getSessionStateBean().getLocale();
-        final EmailItemBean configuredEmailSetting = config.readSettingAsEmail( PwmSetting.EMAIL_ACTIVATION, locale );
-
-        if ( configuredEmailSetting == null )
-        {
-            LOGGER.debug( pwmSession, "skipping send activation email for '" + userInfo.getUserIdentity() + "' no email configured" );
-            return false;
-        }
-
-        pwmApplication.getEmailQueue().submitEmail(
-                configuredEmailSetting,
-                pwmSession.getUserInfo(),
-                pwmSession.getSessionManager().getMacroMachine( pwmApplication )
-        );
-        return true;
-    }
-
-    private Boolean sendPostActivationSms( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final UserInfo userInfo = pwmSession.getUserInfo();
-        final Locale locale = pwmSession.getSessionStateBean().getLocale();
-        final LdapProfile ldapProfile = userInfo.getUserIdentity().getLdapProfile( config );
-
-        final String message = config.readSettingAsLocalizedString( PwmSetting.SMS_ACTIVATION_TEXT, locale );
-
-        final String toSmsNumber;
-        try
-        {
-            toSmsNumber = userInfo.readStringAttribute( ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE ) );
-        }
-        catch ( Exception e )
-        {
-            LOGGER.debug( pwmSession.getLabel(), "error reading SMS attribute from user '" + pwmSession.getUserInfo().getUserIdentity() + "': " + e.getMessage() );
-            return false;
-        }
-
-        if ( toSmsNumber == null || toSmsNumber.length() < 1 )
-        {
-            LOGGER.debug( pwmSession.getLabel(), "skipping send activation SMS for '" + pwmSession.getUserInfo().getUserIdentity() + "' no SMS number configured" );
-            return false;
-        }
-
-        pwmApplication.sendSmsUsingQueue(
-                toSmsNumber,
-                message,
-                pwmRequest.getSessionLabel(),
-                pwmSession.getSessionManager().getMacroMachine( pwmApplication )
-        );
-        return true;
-    }
-
-    private static void initializeToken(
-            final PwmRequest pwmRequest,
-            final Locale locale,
-            final UserIdentity userIdentity
-
-    )
-            throws PwmUnrecoverableException, PwmOperationalException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-        final Configuration config = pwmApplication.getConfig();
-
-        final RestTokenDataClient.TokenDestinationData inputTokenDestData;
-        {
-            final String toAddress;
-            {
-                final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_ACTIVATION_VERIFICATION, locale );
-                final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
-                toAddress = macroMachine.expandMacros( emailItemBean.getTo() );
-            }
-
-            final String toSmsNumber;
-            try
-            {
-                final UserInfo userInfo = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, pwmSession.getLabel(), userIdentity, pwmRequest.getLocale() );
-                final LdapProfile ldapProfile = userIdentity.getLdapProfile( config );
-                toSmsNumber = userInfo.readStringAttribute( ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE ) );
-            }
-            catch ( Exception e )
-            {
-                final String errorMsg = "unable to read user SMS attribute due to ldap error, unable to send token: " + e.getMessage();
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
-                LOGGER.error( pwmSession.getLabel(), errorInformation );
-                throw new PwmOperationalException( errorInformation );
-            }
-            inputTokenDestData = new RestTokenDataClient.TokenDestinationData( toAddress, toSmsNumber, null );
-        }
-
-        final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmApplication );
-        final RestTokenDataClient.TokenDestinationData outputDestTokenData = restTokenDataClient.figureDestTokenDisplayString(
-                pwmSession.getLabel(),
-                inputTokenDestData,
-                activateUserBean.getUserIdentity(),
-                pwmSession.getSessionStateBean().getLocale() );
-
-        final Set<String> destinationValues = new HashSet<>();
-        if ( outputDestTokenData.getEmail() != null )
-        {
-            destinationValues.add( outputDestTokenData.getEmail() );
-        }
-        if ( outputDestTokenData.getSms() != null )
-        {
-            destinationValues.add( outputDestTokenData.getSms() );
-        }
-
-
-        final Map<String, String> tokenMapData = new HashMap<>();
-
-        try
-        {
-            final Instant userLastPasswordChange = PasswordUtility.determinePwdLastModified(
-                    pwmApplication,
-                    pwmSession.getLabel(),
-                    activateUserBean.getUserIdentity() );
-            if ( userLastPasswordChange != null )
-            {
-                tokenMapData.put( PwmConstants.TOKEN_KEY_PWD_CHG_DATE, JavaHelper.toIsoDate( userLastPasswordChange ) );
-            }
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            LOGGER.error( pwmSession.getLabel(), "unexpected error reading user's last password change time" );
-        }
-
-        final String tokenKey;
-        final TokenPayload tokenPayload;
-        try
-        {
-            tokenPayload = pwmApplication.getTokenService().createTokenPayload(
-                    TokenType.ACTIVATION,
-                    new TimeDuration( config.readSettingAsLong( PwmSetting.TOKEN_LIFETIME ), TimeUnit.SECONDS ),
-                    tokenMapData,
-                    userIdentity,
-                    destinationValues
-            );
-            tokenKey = pwmApplication.getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
-        }
-        catch ( PwmOperationalException e )
-        {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
-        }
-
-        final String displayValue = sendToken( pwmRequest, userIdentity, locale, outputDestTokenData.getEmail(), outputDestTokenData.getSms(), tokenKey );
-        activateUserBean.setTokenDisplayText( displayValue );
-        activateUserBean.setTokenIssued( true );
-    }
-
-    private void handleEnterTokenCode(
-            final PwmRequest pwmRequest
-    )
-            throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        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(),
-                    TokenType.ACTIVATION,
-                    userEnteredCode
-            );
-            if ( tokenPayload != null )
-            {
-                activateUserBean.setUserIdentity( tokenPayload.getUserIdentity() );
-                activateUserBean.setTokenPassed( true );
-                activateUserBean.setFormValidated( true );
-            }
-        }
-        catch ( PwmOperationalException e )
-        {
-            final String errorMsg = "token incorrect: " + e.getMessage();
-            errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
-        }
-
-        if ( !activateUserBean.isTokenPassed() )
-        {
-            if ( errorInformation == null )
-            {
-                errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT );
-            }
-            LOGGER.debug( pwmSession.getLabel(), errorInformation.toDebugStr() );
-            setLastError( pwmRequest, errorInformation );
-        }
-
-        this.advanceToNextStage( pwmRequest );
-    }
-
-    private static String sendToken(
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity,
-            final Locale userLocale,
-            final String toAddress,
-            final String toSmsNumber,
-            final String tokenKey
-    )
-            throws PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final Configuration config = pwmApplication.getConfig();
-        final MessageSendMethod pref = MessageSendMethod.valueOf( config.readSettingAsString( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD ) );
-        final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_ACTIVATION_VERIFICATION, userLocale );
-        final String smsMessage = config.readSettingAsLocalizedString( PwmSetting.SMS_ACTIVATION_VERIFICATION_TEXT, userLocale );
-
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
-
-        final List<TokenDestinationItem.Type> sentTypes = TokenService.TokenSender.sendToken(
-                TokenService.TokenSendInfo.builder()
-                        .pwmApplication( pwmApplication )
-                        .userInfo( null )
-                        .macroMachine( macroMachine )
-                        .configuredEmailSetting( emailItemBean )
-                        .tokenSendMethod( pref )
-                        .emailAddress( toAddress )
-                        .smsNumber( toSmsNumber )
-                        .smsMessage( smsMessage )
-                        .tokenKey( tokenKey )
-                        .sessionLabel( pwmRequest.getSessionLabel() )
-                        .build()
-        );
-
-        return TokenService.TokenSender.figureDisplayString(
-                pwmApplication.getConfig(),
-                sentTypes,
-                toAddress,
-                toSmsNumber
-        );
-    }
-
-    private static String figureLdapSearchFilter( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final Configuration config = pwmApplication.getConfig();
-        final List<FormConfiguration> configuredActivationForm = config.readSettingAsForm( PwmSetting.ACTIVATE_USER_FORM );
-
-        final String configuredSearchFilter = config.readSettingAsString( PwmSetting.ACTIVATE_USER_SEARCH_FILTER );
-        final String searchFilter;
-        if ( configuredSearchFilter == null || configuredSearchFilter.isEmpty() )
-        {
-            searchFilter = FormUtility.ldapSearchFilterForForm( pwmApplication, configuredActivationForm );
-            LOGGER.trace( pwmRequest, "auto generated search filter based on activation form: " + searchFilter );
-        }
-        else
-        {
-            searchFilter = configuredSearchFilter;
-        }
-        return searchFilter;
-    }
-
-    private static void forwardToActivateUserForm( final PwmRequest pwmRequest )
-            throws ServletException, PwmUnrecoverableException, IOException
-    {
-        pwmRequest.addFormInfoToRequestAttr( PwmSetting.ACTIVATE_USER_FORM, false, false );
-        pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER );
-    }
-}

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

@@ -40,6 +40,7 @@ import password.pwm.http.bean.SetupResponsesBean;
 import password.pwm.http.bean.ShortcutsBean;
 import password.pwm.http.bean.UpdateProfileBean;
 import password.pwm.http.servlet.accountinfo.AccountInformationServlet;
+import password.pwm.http.servlet.activation.ActivateUserServlet;
 import password.pwm.http.servlet.admin.AdminServlet;
 import password.pwm.http.servlet.changepw.PrivateChangePasswordServlet;
 import password.pwm.http.servlet.changepw.PublicChangePasswordServlet;
@@ -56,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;
@@ -74,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 ),
@@ -94,7 +96,7 @@ public enum PwmServletDefinition
     ConfigManager_PwNotify( ConfigManagerPwNotifyServlet.class, ConfigManagerBean.class ),
 
     NewUser( NewUserServlet.class, NewUserBean.class ),
-    ActivateUser( password.pwm.http.servlet.ActivateUserServlet.class, ActivateUserBean.class ),
+    ActivateUser( ActivateUserServlet.class, ActivateUserBean.class ),
     ForgottenPassword( password.pwm.http.servlet.forgottenpw.ForgottenPasswordServlet.class, ForgottenPasswordBean.class ),
     ForgottenUsername( password.pwm.http.servlet.ForgottenUsernameServlet.class, null ),;
 

+ 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 );
-        }
-    }
-
-
-}
-

+ 487 - 0
server/src/main/java/password/pwm/http/servlet/activation/ActivateUserServlet.java

@@ -0,0 +1,487 @@
+/*
+ * 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.activation;
+
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.LocalSessionStateBean;
+import password.pwm.bean.TokenDestinationItem;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.config.value.data.UserPermission;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+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.PwmHttpRequestWrapper;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.http.PwmSession;
+import password.pwm.http.bean.ActivateUserBean;
+import password.pwm.http.servlet.ControlledPwmServlet;
+import password.pwm.http.servlet.PwmServletDefinition;
+import password.pwm.i18n.Message;
+import password.pwm.ldap.LdapPermissionTester;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoFactory;
+import password.pwm.ldap.search.SearchConfiguration;
+import password.pwm.ldap.search.UserSearchEngine;
+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;
+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 javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * User interaction servlet for creating new users (self registration).
+ *
+ * @author Jason D. Rivard
+ */
+@WebServlet(
+        name = "ActivateUserServlet",
+        urlPatterns = {
+                PwmConstants.URL_PREFIX_PUBLIC + "/activate",
+                PwmConstants.URL_PREFIX_PUBLIC + "/activate/*",
+                PwmConstants.URL_PREFIX_PUBLIC + "/ActivateUser",
+                PwmConstants.URL_PREFIX_PUBLIC + "/ActivateUser/*",
+        }
+)
+public class ActivateUserServlet extends ControlledPwmServlet
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ActivateUserServlet.class );
+
+    public enum ActivateUserAction implements ProcessAction
+    {
+        activate( HttpMethod.POST ),
+        tokenChoice( HttpMethod.POST ),
+        enterCode( HttpMethod.POST, HttpMethod.GET ),
+        reset( HttpMethod.POST ),
+        agree( HttpMethod.POST ),;
+
+        private final Collection<HttpMethod> method;
+
+        ActivateUserAction( final HttpMethod... method )
+        {
+            this.method = Collections.unmodifiableList( Arrays.asList( method ) );
+        }
+
+        public Collection<HttpMethod> permittedMethods( )
+        {
+            return method;
+        }
+    }
+
+    public enum ResetType
+    {
+        exitActivation,
+        clearTokenDestination,
+    }
+
+    @Override
+    public Class<? extends ProcessAction> getProcessActionsClass( )
+    {
+        return ActivateUserAction.class;
+    }
+
+    @Override
+    public ProcessStatus preProcessCheck( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException
+    {
+        final Configuration config = pwmRequest.getConfig();
+
+        if ( !config.readSettingAsBoolean( PwmSetting.ACTIVATE_USER_ENABLE ) )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "activate user is not enabled" );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+
+        if ( pwmRequest.isAuthenticated() )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_USERAUTHENTICATED );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+
+        final ProcessAction action = this.readProcessAction( pwmRequest );
+
+        // convert a url command like /public/newuser/12321321 to redirect with a process action.
+        if ( action == null )
+        {
+            if ( pwmRequest.convertURLtokenCommand( PwmServletDefinition.ActivateUser, ActivateUserAction.enterCode ) )
+            {
+                return ProcessStatus.Halt;
+            }
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    static UserInfo userInfo ( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    {
+        final ActivateUserBean activateUserBean = activateUserBean( pwmRequest );
+        return UserInfoFactory.newUserInfoUsingProxy(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                activateUserBean.getUserIdentity(),
+                pwmRequest.getLocale() );
+
+    }
+
+    static ActivateUserBean activateUserBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    {
+        return pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
+    }
+
+    @ActionHandler( action = "reset" )
+    public ProcessStatus handleResetRequest( final PwmRequest pwmRequest )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        final ResetType resetType = pwmRequest.readParameterAsEnum( PwmConstants.PARAM_RESET_TYPE, ResetType.class, ResetType.exitActivation );
+
+        switch ( resetType )
+        {
+            case exitActivation:
+                pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, ActivateUserBean.class );
+                ActivateUserUtils.forwardToActivateUserForm( pwmRequest );
+                return ProcessStatus.Halt;
+
+            case clearTokenDestination:
+                activateUserBean( pwmRequest ).setTokenDestination( null );
+                activateUserBean( pwmRequest ).setTokenSent( false );
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( resetType );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "activate" )
+    public ProcessStatus handleActivateRequest( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final LocalSessionStateBean ssBean = pwmSession.getSessionStateBean();
+
+        if ( CaptchaUtility.captchaEnabledForRequest( pwmRequest ) )
+        {
+            if ( !CaptchaUtility.verifyReCaptcha( pwmRequest ) )
+            {
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_BAD_CAPTCHA_RESPONSE );
+                throw new PwmUnrecoverableException( errorInfo );
+            }
+        }
+
+        pwmApplication.getSessionStateService().clearBean( pwmRequest, ActivateUserBean.class );
+        final List<FormConfiguration> configuredActivationForm = config.readSettingAsForm( PwmSetting.ACTIVATE_USER_FORM );
+
+        Map<FormConfiguration, String> formValues = new HashMap<>();
+        try
+        {
+            //read the values from the request
+            formValues = FormUtility.readFormValuesFromRequest( pwmRequest, configuredActivationForm,
+                    ssBean.getLocale() );
+
+            // check for intruders
+            pwmApplication.getIntruderManager().convenience().checkAttributes( formValues );
+
+            // read the context attr
+            final String contextParam = pwmRequest.readParameterAsString( PwmConstants.PARAM_CONTEXT );
+
+            // read the profile attr
+            final String ldapProfile = pwmRequest.readParameterAsString( PwmConstants.PARAM_LDAP_PROFILE );
+
+            // see if the values meet the configured form requirements.
+            FormUtility.validateFormValues( config, formValues, ssBean.getLocale() );
+
+            final String searchFilter = ActivateUserUtils.figureLdapSearchFilter( pwmRequest );
+
+            // read an ldap user object based on the params
+            final UserIdentity userIdentity;
+            {
+                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+                final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                        .contexts( Collections.singletonList( contextParam ) )
+                        .filter( searchFilter )
+                        .formValues( formValues )
+                        .ldapProfile( ldapProfile )
+                        .build();
+
+                userIdentity = userSearchEngine.performSingleUserSearch( searchConfiguration, pwmRequest.getSessionLabel() );
+            }
+
+            ActivateUserUtils.validateParamsAgainstLDAP( pwmRequest, formValues, userIdentity );
+
+            final List<UserPermission> userPermissions = config.readSettingAsUserPermission( PwmSetting.ACTIVATE_USER_QUERY_MATCH );
+            if ( !LdapPermissionTester.testUserPermissions( pwmApplication, pwmSession.getLabel(), userIdentity, userPermissions ) )
+            {
+                final String errorMsg = "user " + userIdentity + " attempted activation, but does not match query string";
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATE_NO_PERMISSION, errorMsg );
+                pwmApplication.getIntruderManager().convenience().markUserIdentity( userIdentity, pwmSession );
+                pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+
+            final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
+            activateUserBean.setUserIdentity( userIdentity );
+            activateUserBean.setFormValidated( true );
+            pwmApplication.getIntruderManager().convenience().clearAttributes( formValues );
+            pwmApplication.getIntruderManager().convenience().clearAddressAndSession( pwmSession );
+        }
+        catch ( PwmOperationalException e )
+        {
+            pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmSession );
+            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
+            setLastError( pwmRequest, e.getErrorInformation() );
+            LOGGER.debug( pwmSession.getLabel(), e.getErrorInformation().toDebugStr() );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "tokenChoice" )
+    private ProcessStatus processTokenChoice( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        final ActivateUserBean activateUserBean = activateUserBean( pwmRequest );
+        final UserInfo userInfo = userInfo( pwmRequest );
+        final MessageSendMethod tokenSendMethod = pwmRequest.getConfig().readSettingAsEnum( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD, MessageSendMethod.class );
+
+        final List<TokenDestinationItem> tokenDestinationItems = TokenUtil.figureAvailableTokenDestinations(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                pwmRequest.getLocale(),
+                userInfo,
+                tokenSendMethod
+        );
+
+        final String requestedID = pwmRequest.readParameterAsString( "choice", PwmHttpRequestWrapper.Flag.BypassValidation );
+
+        if ( !StringUtil.isEmpty( requestedID ) )
+        {
+            for ( final TokenDestinationItem item : tokenDestinationItems )
+            {
+                if ( requestedID.equals( item.getId() ) )
+                {
+                    activateUserBean.setTokenDestination( item );
+                }
+            }
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+
+    @ActionHandler( action = "enterCode" )
+    public ProcessStatus handleEnterCode(
+            final PwmRequest pwmRequest
+    )
+            throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
+        final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
+
+
+        ErrorInformation errorInformation = null;
+        try
+        {
+            final TokenPayload tokenPayload = TokenUtil.checkEnteredCode(
+                    pwmRequest,
+                    userEnteredCode,
+                    activateUserBean.getTokenDestination(),
+                    null,
+                    TokenType.ACTIVATION,
+                    TokenService.TokenEntryType.unauthenticated
+            );
+
+            activateUserBean.setUserIdentity( tokenPayload.getUserIdentity() );
+            activateUserBean.setTokenPassed( true );
+            activateUserBean.setFormValidated( true );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.debug( pwmRequest, "error while checking entered token: " );
+            errorInformation = e.getErrorInformation();
+        }
+
+
+        if ( !activateUserBean.isTokenPassed() )
+        {
+            if ( errorInformation == null )
+            {
+                errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT );
+            }
+            LOGGER.debug( pwmSession.getLabel(), errorInformation.toDebugStr() );
+            setLastError( pwmRequest, errorInformation );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "agree" )
+    public ProcessStatus handleAgreeRequest(
+            final PwmRequest pwmRequest
+    )
+            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException
+    {
+        LOGGER.debug( pwmRequest, "user accepted agreement" );
+
+        final ActivateUserBean activateUserBean = activateUserBean( pwmRequest );
+
+        if ( !activateUserBean.isAgreementPassed() )
+        {
+            activateUserBean.setAgreementPassed( true );
+            final AuditRecord auditRecord = new AuditRecordFactory( pwmRequest ).createUserAuditRecord(
+                    AuditEvent.AGREEMENT_PASSED,
+                    pwmRequest.getUserInfoIfLoggedIn(),
+                    pwmRequest.getSessionLabel(),
+                    "ActivateUser"
+            );
+            pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @Override
+    protected void nextStep( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ChaiUnavailableException, ServletException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final ActivateUserBean activateUserBean = activateUserBean( pwmRequest );
+
+        if ( !activateUserBean.isFormValidated() || activateUserBean.getUserIdentity() == null )
+        {
+            ActivateUserUtils.forwardToActivateUserForm( pwmRequest );
+            return;
+        }
+
+        final UserInfo userInfo = userInfo( pwmRequest );
+
+        final MessageSendMethod tokenSendMethod = config.readSettingAsEnum( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD, MessageSendMethod.class );
+        if ( MessageSendMethod.NONE != tokenSendMethod )
+        {
+            final List<TokenDestinationItem> tokenDestinationItems = TokenUtil.figureAvailableTokenDestinations(
+                    pwmApplication,
+                    pwmRequest.getSessionLabel(),
+                    pwmRequest.getLocale(),
+                    userInfo,
+                    tokenSendMethod
+            );
+
+            if ( activateUserBean.getTokenDestination() == null )
+            {
+                if ( tokenDestinationItems.size() == 1 )
+                {
+                    activateUserBean.setTokenDestination( tokenDestinationItems.iterator().next() );
+                }
+                else if ( tokenDestinationItems.size() > 1 )
+                {
+                    forwardToTokenChoiceJsp( pwmRequest, tokenDestinationItems );
+                    return;
+                }
+            }
+
+            if ( !activateUserBean.isTokenSent() && activateUserBean.getTokenDestination() != null )
+            {
+
+                TokenUtil.initializeAndSendToken(
+                        pwmRequest,
+                        userInfo,
+                        activateUserBean.getTokenDestination(),
+                        PwmSetting.EMAIL_ACTIVATION_VERIFICATION,
+                        TokenType.ACTIVATION,
+                        PwmSetting.SMS_ACTIVATION_VERIFICATION_TEXT
+                );
+            }
+
+            if ( !activateUserBean.isTokenPassed() )
+            {
+                pwmRequest.setAttribute( PwmRequestAttribute.ShowGoBackButton, tokenDestinationItems.size() > 1 );
+                pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_ENTER_CODE );
+                return;
+            }
+        }
+
+        final String agreementText = config.readSettingAsLocalizedString(
+                PwmSetting.ACTIVATE_AGREEMENT_MESSAGE,
+                pwmSession.getSessionStateBean().getLocale()
+        );
+        if ( !StringUtil.isEmpty( agreementText ) && !activateUserBean.isAgreementPassed() )
+        {
+            ActivateUserUtils.forwardToAgreementPage( pwmRequest );
+            return;
+        }
+
+        try
+        {
+            ActivateUserUtils.activateUser( pwmRequest, activateUserBean.getUserIdentity() );
+            pwmRequest.getPwmResponse().forwardToSuccessPage( Message.Success_ActivateUser );
+        }
+        catch ( PwmOperationalException e )
+        {
+            LOGGER.debug( pwmRequest, e.getErrorInformation() );
+            pwmApplication.getIntruderManager().convenience().markUserIdentity( activateUserBean.getUserIdentity(), pwmSession );
+            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
+            pwmRequest.respondWithError( e.getErrorInformation() );
+        }
+    }
+
+
+    private void forwardToTokenChoiceJsp( final PwmRequest pwmRequest, final List<TokenDestinationItem> tokenDestinationItems )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        pwmRequest.setAttribute( PwmRequestAttribute.TokenDestItems, new ArrayList<>( tokenDestinationItems ) );
+        pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_TOKEN_CHOICE );
+    }
+
+}

+ 390 - 0
server/src/main/java/password/pwm/http/servlet/activation/ActivateUserUtils.java

@@ -0,0 +1,390 @@
+/*
+ * 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.activation;
+
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiOperationException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
+import com.novell.ldapchai.provider.ChaiProvider;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import password.pwm.PwmApplication;
+import password.pwm.bean.EmailItemBean;
+import password.pwm.bean.LoginInfoBean;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.profile.LdapProfile;
+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.JspUrl;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.http.PwmSession;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.auth.AuthenticationType;
+import password.pwm.ldap.auth.PwmAuthenticationSource;
+import password.pwm.ldap.auth.SessionAuthenticator;
+import password.pwm.svc.event.AuditEvent;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.util.PostChangePasswordAction;
+import password.pwm.util.form.FormUtility;
+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.List;
+import java.util.Locale;
+import java.util.Map;
+
+class ActivateUserUtils
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ActivateUserUtils.class );
+
+    private ActivateUserUtils()
+    {
+    }
+
+
+    @SuppressFBWarnings( "SE_BAD_FIELD" )
+    static void activateUser(
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity
+    )
+            throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
+        if ( config.readSettingAsBoolean( PwmSetting.ACTIVATE_USER_UNLOCK ) )
+        {
+            try
+            {
+                theUser.unlockPassword();
+            }
+            catch ( ChaiOperationException e )
+            {
+                final String errorMsg = "error unlocking user " + userIdentity + ": " + e.getMessage();
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
+                throw new PwmOperationalException( errorInformation );
+            }
+        }
+
+        try
+        {
+            {
+                // execute configured actions
+                LOGGER.debug( pwmSession.getLabel(), "executing configured pre-actions to user " + theUser.getEntryDN() );
+                final List<ActionConfiguration> configValues = config.readSettingAsAction( PwmSetting.ACTIVATE_USER_PRE_WRITE_ATTRIBUTES );
+                if ( configValues != null && !configValues.isEmpty() )
+                {
+                    final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
+
+                    final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
+                            .setExpandPwmMacros( true )
+                            .setMacroMachine( macroMachine )
+                            .createActionExecutor();
+
+                    actionExecutor.executeActions( configValues, pwmRequest.getSessionLabel() );
+                }
+            }
+
+            //authenticate the pwm session
+            final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator( pwmApplication, pwmSession, PwmAuthenticationSource.USER_ACTIVATION );
+            sessionAuthenticator.authUserWithUnknownPassword( userIdentity, AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
+
+            //ensure a change password is triggered
+            pwmSession.getLoginInfoBean().setType( AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
+            pwmSession.getLoginInfoBean().getAuthFlags().add( AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
+            pwmSession.getLoginInfoBean().getLoginFlags().add( LoginInfoBean.LoginFlag.forcePwChange );
+
+
+            // mark the event log
+            pwmApplication.getAuditManager().submit( AuditEvent.ACTIVATE_USER, pwmSession.getUserInfo(), pwmSession );
+
+            // update the stats bean
+            pwmApplication.getStatisticsManager().incrementValue( Statistic.ACTIVATED_USERS );
+
+            // send email or sms
+            sendPostActivationNotice( pwmRequest );
+
+            // setup post-change attributes
+            final PostChangePasswordAction postAction = new PostChangePasswordAction()
+            {
+
+                public String getLabel( )
+                {
+                    return "ActivateUser write attributes";
+                }
+
+                public boolean doAction( final PwmSession pwmSession, final String newPassword )
+                        throws PwmUnrecoverableException
+                {
+                    try
+                    {
+                        {
+                            // execute configured actions
+                            LOGGER.debug( pwmSession.getLabel(), "executing post-activate configured actions to user " + userIdentity.toDisplayString() );
+
+                            final MacroMachine macroMachine = pwmSession.getSessionManager().getMacroMachine( pwmApplication );
+                            final List<ActionConfiguration> configValues = pwmApplication.getConfig().readSettingAsAction( PwmSetting.ACTIVATE_USER_POST_WRITE_ATTRIBUTES );
+
+                            final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
+                                    .setExpandPwmMacros( true )
+                                    .setMacroMachine( macroMachine )
+                                    .createActionExecutor();
+                            actionExecutor.executeActions( configValues, pwmRequest.getSessionLabel() );
+                        }
+                    }
+                    catch ( PwmOperationalException e )
+                    {
+                        final ErrorInformation info = new ErrorInformation(
+                                PwmError.ERROR_ACTIVATION_FAILURE,
+                                e.getErrorInformation().getDetailedErrorMsg(), e.getErrorInformation().getFieldValues()
+                        );
+                        final PwmUnrecoverableException newException = new PwmUnrecoverableException( info );
+                        newException.initCause( e );
+                        throw newException;
+                    }
+                    catch ( ChaiUnavailableException e )
+                    {
+                        final String errorMsg = "unable to reach ldap server while writing post-activate attributes: " + e.getMessage();
+                        final ErrorInformation info = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
+                        final PwmUnrecoverableException newException = new PwmUnrecoverableException( info );
+                        newException.initCause( e );
+                        throw newException;
+                    }
+                    return true;
+                }
+            };
+
+            pwmSession.getUserSessionDataCacheBean().addPostChangePasswordActions( "activateUserWriteAttributes", postAction );
+        }
+        catch ( ImpossiblePasswordPolicyException e )
+        {
+            final ErrorInformation info = new ErrorInformation( PwmError.ERROR_UNKNOWN, "unexpected ImpossiblePasswordPolicyException error while activating user" );
+            LOGGER.warn( pwmSession, info, e );
+            throw new PwmOperationalException( info );
+        }
+    }
+
+    static void validateParamsAgainstLDAP(
+            final PwmRequest pwmRequest,
+            final Map<FormConfiguration, String> formValues,
+            final UserIdentity userIdentity
+    )
+            throws ChaiUnavailableException, PwmDataValidationException, PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final String searchFilter = figureLdapSearchFilter( pwmRequest );
+        final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider( userIdentity.getLdapProfileID() );
+        final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser( userIdentity.getUserDN() );
+
+        for ( final Map.Entry<FormConfiguration, String> entry : formValues.entrySet() )
+        {
+            final FormConfiguration formItem = entry.getKey();
+            final String attrName = formItem.getName();
+            final String tokenizedAttrName = "%" + attrName + "%";
+            if ( searchFilter.contains( tokenizedAttrName ) )
+            {
+                LOGGER.trace( pwmSession, "skipping validation of ldap value for '" + attrName + "' because it is in search filter" );
+            }
+            else
+            {
+                final String value = entry.getValue();
+                try
+                {
+                    if ( !chaiUser.compareStringAttribute( attrName, value ) )
+                    {
+                        final String errorMsg = "incorrect value for '" + attrName + "'";
+                        final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_ACTIVATION_VALIDATIONFAIL, errorMsg, new String[]
+                                {
+                                        attrName,
+                                }
+                        );
+                        LOGGER.debug( pwmSession.getLabel(), errorInfo.toDebugStr() );
+                        throw new PwmDataValidationException( errorInfo );
+                    }
+                    LOGGER.trace( pwmSession.getLabel(), "successful validation of ldap value for '" + attrName + "'" );
+                }
+                catch ( ChaiOperationException e )
+                {
+                    LOGGER.error( pwmSession.getLabel(), "error during param validation of '" + attrName + "', error: " + e.getMessage() );
+                    throw new PwmDataValidationException( new ErrorInformation(
+                            PwmError.ERROR_ACTIVATION_VALIDATIONFAIL,
+                            "ldap error testing value for '" + attrName + "'", new String[]
+                            {
+                                    attrName,
+                            }
+                    ) );
+                }
+            }
+        }
+    }
+
+    static void sendPostActivationNotice(
+            final PwmRequest pwmRequest
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final UserInfo userInfo = pwmSession.getUserInfo();
+        final MessageSendMethod pref = MessageSendMethod.valueOf( config.readSettingAsString( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD ) );
+
+        final boolean success;
+        switch ( pref )
+        {
+            case SMSONLY:
+                // Only try SMS
+                success = sendPostActivationSms( pwmRequest );
+                break;
+            case EMAILONLY:
+            default:
+                // Only try email
+                success = sendPostActivationEmail( pwmRequest );
+                break;
+        }
+        if ( !success )
+        {
+            LOGGER.warn( pwmSession, "skipping send activation message for '" + userInfo.getUserIdentity() + "' no email or SMS number configured" );
+        }
+    }
+
+    static boolean sendPostActivationEmail(
+            final PwmRequest pwmRequest
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final UserInfo userInfo = pwmSession.getUserInfo();
+        final Configuration config = pwmApplication.getConfig();
+        final Locale locale = pwmSession.getSessionStateBean().getLocale();
+        final EmailItemBean configuredEmailSetting = config.readSettingAsEmail( PwmSetting.EMAIL_ACTIVATION, locale );
+
+        if ( configuredEmailSetting == null )
+        {
+            LOGGER.debug( pwmSession, "skipping send activation email for '" + userInfo.getUserIdentity() + "' no email configured" );
+            return false;
+        }
+
+        pwmApplication.getEmailQueue().submitEmail(
+                configuredEmailSetting,
+                pwmSession.getUserInfo(),
+                pwmSession.getSessionManager().getMacroMachine( pwmApplication )
+        );
+        return true;
+    }
+
+    static boolean sendPostActivationSms( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final UserInfo userInfo = pwmSession.getUserInfo();
+        final Locale locale = pwmSession.getSessionStateBean().getLocale();
+        final LdapProfile ldapProfile = userInfo.getUserIdentity().getLdapProfile( config );
+
+        final String message = config.readSettingAsLocalizedString( PwmSetting.SMS_ACTIVATION_TEXT, locale );
+
+        final String toSmsNumber;
+        try
+        {
+            toSmsNumber = userInfo.readStringAttribute( ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE ) );
+        }
+        catch ( Exception e )
+        {
+            LOGGER.debug( pwmSession.getLabel(), "error reading SMS attribute from user '" + pwmSession.getUserInfo().getUserIdentity() + "': " + e.getMessage() );
+            return false;
+        }
+
+        if ( toSmsNumber == null || toSmsNumber.length() < 1 )
+        {
+            LOGGER.debug( pwmSession.getLabel(), "skipping send activation SMS for '" + pwmSession.getUserInfo().getUserIdentity() + "' no SMS number configured" );
+            return false;
+        }
+
+        pwmApplication.sendSmsUsingQueue(
+                toSmsNumber,
+                message,
+                pwmRequest.getSessionLabel(),
+                pwmSession.getSessionManager().getMacroMachine( pwmApplication )
+        );
+        return true;
+    }
+
+    static String figureLdapSearchFilter( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final Configuration config = pwmApplication.getConfig();
+        final List<FormConfiguration> configuredActivationForm = config.readSettingAsForm( PwmSetting.ACTIVATE_USER_FORM );
+
+        final String configuredSearchFilter = config.readSettingAsString( PwmSetting.ACTIVATE_USER_SEARCH_FILTER );
+        final String searchFilter;
+        if ( configuredSearchFilter == null || configuredSearchFilter.isEmpty() )
+        {
+            searchFilter = FormUtility.ldapSearchFilterForForm( pwmApplication, configuredActivationForm );
+            LOGGER.trace( pwmRequest, "auto generated search filter based on activation form: " + searchFilter );
+        }
+        else
+        {
+            searchFilter = configuredSearchFilter;
+        }
+        return searchFilter;
+    }
+
+    static void forwardToAgreementPage( final PwmRequest pwmRequest )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        final String agreementText = pwmRequest.getConfig().readSettingAsLocalizedString(
+                PwmSetting.ACTIVATE_AGREEMENT_MESSAGE,
+                pwmRequest.getLocale()
+        );
+
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, ActivateUserServlet.userInfo( pwmRequest ).getUserIdentity() );
+        final String expandedText = macroMachine.expandMacros( agreementText );
+        pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
+        pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_AGREEMENT );
+    }
+
+    static void forwardToActivateUserForm( final PwmRequest pwmRequest )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        pwmRequest.addFormInfoToRequestAttr( PwmSetting.ACTIVATE_USER_FORM, false, false );
+        pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER );
+    }
+}

+ 27 - 18
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 )
         {
@@ -1506,7 +1515,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
         final List<TokenDestinationItem> destItems = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
-        pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordTokenDestItems, new ArrayList<>( destItems ) );
+        pwmRequest.setAttribute( PwmRequestAttribute.TokenDestItems, new ArrayList<>( destItems ) );
         pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_TOKEN_CHOICE );
     }
 }

+ 16 - 136
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -63,23 +63,17 @@ import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 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.PasswordData;
 import password.pwm.util.RandomPasswordGenerator;
-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;
 import password.pwm.util.operations.PasswordUtility;
-import password.pwm.ws.client.rest.RestTokenDataClient;
 
 import javax.servlet.ServletException;
 import java.io.IOException;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -88,8 +82,6 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 
 public class ForgottenPasswordUtil
 {
@@ -298,38 +290,17 @@ public class ForgottenPasswordUtil
         final String profileID = forgottenPasswordBean.getForgottenPasswordProfileID();
         final ForgottenPasswordProfile forgottenPasswordProfile = pwmRequest.getConfig().getForgottenPasswordProfiles().get( profileID );
         final MessageSendMethod tokenSendMethod = forgottenPasswordProfile.readSettingAsEnum( PwmSetting.RECOVERY_TOKEN_SEND_METHOD, MessageSendMethod.class );
-        if ( tokenSendMethod == null || tokenSendMethod.equals( MessageSendMethod.NONE ) )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, "no token send methods configured in profile" );
-        }
-
         final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-        List<TokenDestinationItem> tokenDestinations = new ArrayList<>( TokenDestinationItem.allFromConfig( pwmRequest.getPwmApplication(), userInfo ) );
-
-        if ( tokenSendMethod != MessageSendMethod.CHOICE_SMS_EMAIL )
-        {
-            tokenDestinations = tokenDestinations
-                    .stream()
-                    .filter( tokenDestinationItem -> tokenSendMethod == tokenDestinationItem.getType().getMessageSendMethod() )
-                    .collect( Collectors.toList() );
-        }
-
-        final List<TokenDestinationItem> effectiveItems = new ArrayList<>(  );
-        for ( final TokenDestinationItem item : tokenDestinations )
-        {
-            final TokenDestinationItem effectiveItem = invokeExternalTokenDestRestClient( pwmRequest, userInfo.getUserIdentity(), item );
-            effectiveItems.add( effectiveItem );
-        }
 
-        LOGGER.trace( pwmRequest, "calculated available token send destinations: " + JsonUtil.serializeCollection( effectiveItems ) );
-
-        if ( tokenDestinations.isEmpty() )
-        {
-            final String msg = "no available contact methods of type " + tokenSendMethod.name() + " available";
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, msg );
-        }
+        final List<TokenDestinationItem> items = TokenUtil.figureAvailableTokenDestinations(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                pwmRequest.getLocale(),
+                userInfo,
+                tokenSendMethod
+        );
 
-        final List<TokenDestinationItem> finalList = Collections.unmodifiableList( effectiveItems );
+        final List<TokenDestinationItem> finalList = Collections.unmodifiableList( items );
         pwmRequest.getHttpServletRequest().setAttribute( PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE, finalList );
 
         return finalList;
@@ -444,109 +415,18 @@ public class ForgottenPasswordUtil
     )
             throws PwmUnrecoverableException
     {
-        final Configuration config = pwmRequest.getConfig();
-        final UserIdentity userIdentity = userInfo.getUserIdentity();
-        final Map<String, String> tokenMapData = new LinkedHashMap<>();
-
-        try
-        {
-            final Instant userLastPasswordChange = PasswordUtility.determinePwdLastModified(
-                    pwmRequest.getPwmApplication(),
-                    pwmRequest.getSessionLabel(),
-                    userIdentity
-            );
-            if ( userLastPasswordChange != null )
-            {
-                final String userChangeString = JavaHelper.toIsoDate( userLastPasswordChange );
-                tokenMapData.put( PwmConstants.TOKEN_KEY_PWD_CHG_DATE, userChangeString );
-            }
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            LOGGER.error( pwmRequest, "unexpected error reading user's last password change time" );
-        }
-
-        final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_CHALLENGE_TOKEN, pwmRequest.getLocale() );
-        final MacroMachine.StringReplacer stringReplacer = ( matchedMacro, newValue ) ->
-        {
-            if ( "@User:Email@".equals( matchedMacro )  )
-            {
-                return tokenDestinationItem.getValue();
-            }
-
-            return newValue;
-        };
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity, stringReplacer );
-
-        final String tokenKey;
-        final TokenPayload tokenPayload;
-        try
-        {
-            tokenPayload = pwmRequest.getPwmApplication().getTokenService().createTokenPayload(
-                    TokenType.FORGOTTEN_PW,
-                    new TimeDuration( config.readSettingAsLong( PwmSetting.TOKEN_LIFETIME ), TimeUnit.SECONDS ),
-                    tokenMapData,
-                    userIdentity,
-                    Collections.singleton( tokenDestinationItem.getValue() )
-            );
-            tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
-        }
-        catch ( PwmOperationalException e )
-        {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
-        }
-
-        final String smsMessage = config.readSettingAsLocalizedString( PwmSetting.SMS_CHALLENGE_TOKEN_TEXT, pwmRequest.getLocale() );
-
-        TokenService.TokenSender.sendToken(
-                TokenService.TokenSendInfo.builder()
-                        .pwmApplication( pwmRequest.getPwmApplication() )
-                        .userInfo( userInfo )
-                        .macroMachine( macroMachine )
-                        .configuredEmailSetting( emailItemBean )
-                        .tokenSendMethod( tokenDestinationItem.getType().getMessageSendMethod() )
-                        .emailAddress( tokenDestinationItem.getValue() )
-                        .smsNumber( tokenDestinationItem.getValue() )
-                        .smsMessage( smsMessage )
-                        .tokenKey( tokenKey )
-                        .sessionLabel( pwmRequest.getSessionLabel() )
-                        .build()
+        TokenUtil.initializeAndSendToken(
+                pwmRequest,
+                userInfo,
+                tokenDestinationItem,
+                PwmSetting.EMAIL_CHALLENGE_TOKEN,
+                TokenType.FORGOTTEN_PW,
+                PwmSetting.SMS_CHALLENGE_TOKEN_TEXT
         );
 
         StatisticsManager.incrementStat( pwmRequest, Statistic.RECOVERY_TOKENS_SENT );
     }
 
-    private static TokenDestinationItem invokeExternalTokenDestRestClient(
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity,
-            final TokenDestinationItem tokenDestinationItem
-    )
-            throws PwmUnrecoverableException
-    {
-        final RestTokenDataClient.TokenDestinationData inputDestinationData = new RestTokenDataClient.TokenDestinationData(
-                tokenDestinationItem.getType() == TokenDestinationItem.Type.email ? tokenDestinationItem.getValue() : null,
-                tokenDestinationItem.getType() == TokenDestinationItem.Type.sms ? tokenDestinationItem.getValue() : null,
-                tokenDestinationItem.getDisplay()
-        );
-
-        final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmRequest.getPwmApplication() );
-        final RestTokenDataClient.TokenDestinationData outputDestrestTokenDataClient = restTokenDataClient.figureDestTokenDisplayString(
-                pwmRequest.getSessionLabel(),
-                inputDestinationData,
-                userIdentity,
-                pwmRequest.getLocale() );
-
-        final String outputValue = tokenDestinationItem.getType() == TokenDestinationItem.Type.email
-                ? outputDestrestTokenDataClient.getEmail()
-                : outputDestrestTokenDataClient.getSms();
-
-        return TokenDestinationItem.builder()
-                .type( tokenDestinationItem.getType() )
-                .display( outputDestrestTokenDataClient.getDisplayValue() )
-                .value( outputValue )
-                .id( tokenDestinationItem.getId() )
-                .build();
-    }
 
     static void doActionSendNewPassword( final PwmRequest pwmRequest )
             throws ChaiUnavailableException, IOException, ServletException, PwmUnrecoverableException

+ 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.util.ValueObfuscator;
+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 ValueObfuscator valueObfuscator = new ValueObfuscator( pwmApplication.getConfig() );
-                newUserBean.getTokenVerificationProgress().setTokenDisplayText( valueObfuscator.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 ValueObfuscator valueObfuscator = new ValueObfuscator( pwmApplication.getConfig() );
-                newUserBean.getTokenVerificationProgress().setTokenDisplayText( valueObfuscator.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
 

+ 16 - 3
server/src/main/java/password/pwm/util/ValueObfuscator.java → server/src/main/java/password/pwm/svc/token/TokenDestinationDisplayMasker.java

@@ -20,23 +20,31 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util;
+package password.pwm.svc.token;
 
 import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
 import password.pwm.util.java.StringUtil;
 
-public class ValueObfuscator
+public class TokenDestinationDisplayMasker
 {
     private final Configuration configuration;
+    private boolean enabled;
 
-    public ValueObfuscator( final Configuration configuration )
+    public TokenDestinationDisplayMasker( final Configuration configuration )
     {
         this.configuration = configuration;
+        this.enabled = configuration.readSettingAsBoolean( PwmSetting.TOKEN_ENABLE_VALUE_MASKING );
     }
 
     public String maskEmail( final String email )
     {
+        if ( !enabled )
+        {
+            return email;
+        }
+
         if ( StringUtil.isEmpty( email ) )
         {
             return "";
@@ -49,6 +57,11 @@ public class ValueObfuscator
 
     public String maskPhone( final String phone )
     {
+        if ( !enabled )
+        {
+            return phone;
+        }
+
         if ( StringUtil.isEmpty( phone ) )
         {
             return "";

+ 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;
     }
 

+ 16 - 14
server/src/main/java/password/pwm/svc/token/TokenService.java

@@ -57,7 +57,6 @@ import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.DataStore;
-import password.pwm.util.ValueObfuscator;
 import password.pwm.util.db.DatabaseDataStore;
 import password.pwm.util.db.DatabaseTable;
 import password.pwm.util.java.JavaHelper;
@@ -77,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;
@@ -111,6 +109,12 @@ public class TokenService implements PwmService
 
     private boolean verifyPwModifyTime = true;
 
+    public enum TokenEntryType
+    {
+        unauthenticated,
+        authenticated,
+    }
+
     public TokenService( )
             throws PwmOperationalException
     {
@@ -121,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 ->
@@ -146,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 )
@@ -571,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
     {
@@ -583,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;
@@ -607,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 );
@@ -826,12 +828,12 @@ public class TokenService implements PwmService
                 final String sms
         )
         {
-            final ValueObfuscator valueObfuscator = new ValueObfuscator( configuration );
+            final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( configuration );
             final StringBuilder displayDestAddress = new StringBuilder();
             {
                 if ( sentTypes.contains( TokenDestinationItem.Type.email ) )
                 {
-                    displayDestAddress.append( valueObfuscator.maskEmail( email ) );
+                    displayDestAddress.append( tokenDestinationDisplayMasker.maskEmail( email ) );
                 }
 
                 if ( sentTypes.contains( TokenDestinationItem.Type.sms ) )
@@ -840,7 +842,7 @@ public class TokenService implements PwmService
                     {
                         displayDestAddress.append( " & " );
                     }
-                    displayDestAddress.append( valueObfuscator.maskPhone( sms ) );
+                    displayDestAddress.append( tokenDestinationDisplayMasker.maskPhone( sms ) );
                 }
             }
             return displayDestAddress.toString();

+ 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;
 

+ 299 - 0
server/src/main/java/password/pwm/svc/token/TokenUtil.java

@@ -0,0 +1,299 @@
+/*
+ * 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.svc.token;
+
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+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.option.MessageSendMethod;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+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;
+import password.pwm.ws.client.rest.RestTokenDataClient;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+public class TokenUtil
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( TokenUtil.class );
+
+    private TokenUtil()
+    {
+    }
+
+
+    public static List<TokenDestinationItem> figureAvailableTokenDestinations(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final Locale locale,
+            final UserInfo userInfo,
+            final MessageSendMethod tokenSendMethod
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( tokenSendMethod == null || tokenSendMethod.equals( MessageSendMethod.NONE ) )
+        {
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, "no token send methods configured in profile" );
+        }
+
+        List<TokenDestinationItem> tokenDestinations = new ArrayList<>( TokenDestinationItem.allFromConfig( pwmApplication, userInfo ) );
+
+        if ( tokenSendMethod != MessageSendMethod.CHOICE_SMS_EMAIL )
+        {
+            tokenDestinations = tokenDestinations
+                    .stream()
+                    .filter( tokenDestinationItem -> tokenSendMethod == tokenDestinationItem.getType().getMessageSendMethod() )
+                    .collect( Collectors.toList() );
+        }
+
+        final List<TokenDestinationItem> effectiveItems = new ArrayList<>(  );
+        for ( final TokenDestinationItem item : tokenDestinations )
+        {
+            final TokenDestinationItem effectiveItem = invokeExternalTokenDestRestClient( pwmApplication, sessionLabel, locale, userInfo.getUserIdentity(), item );
+            effectiveItems.add( effectiveItem );
+        }
+
+        LOGGER.trace( sessionLabel, "calculated available token send destinations: " + JsonUtil.serializeCollection( effectiveItems ) );
+
+        if ( tokenDestinations.isEmpty() )
+        {
+            final String msg = "no available contact methods of type " + tokenSendMethod.name() + " available";
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, msg );
+        }
+
+        return Collections.unmodifiableList( effectiveItems );
+    }
+
+    private static TokenDestinationItem invokeExternalTokenDestRestClient(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final Locale locale,
+            final UserIdentity userIdentity,
+            final TokenDestinationItem tokenDestinationItem
+    )
+            throws PwmUnrecoverableException
+    {
+        final RestTokenDataClient.TokenDestinationData inputDestinationData = new RestTokenDataClient.TokenDestinationData(
+                tokenDestinationItem.getType() == TokenDestinationItem.Type.email ? tokenDestinationItem.getValue() : null,
+                tokenDestinationItem.getType() == TokenDestinationItem.Type.sms ? tokenDestinationItem.getValue() : null,
+                tokenDestinationItem.getDisplay()
+        );
+
+        final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmApplication );
+        final RestTokenDataClient.TokenDestinationData outputDestrestTokenDataClient = restTokenDataClient.figureDestTokenDisplayString(
+                sessionLabel,
+                inputDestinationData,
+                userIdentity,
+                locale );
+
+        final String outputValue = tokenDestinationItem.getType() == TokenDestinationItem.Type.email
+                ? outputDestrestTokenDataClient.getEmail()
+                : outputDestrestTokenDataClient.getSms();
+
+        return TokenDestinationItem.builder()
+                .type( tokenDestinationItem.getType() )
+                .display( outputDestrestTokenDataClient.getDisplayValue() )
+                .value( outputValue )
+                .id( tokenDestinationItem.getId() )
+                .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,
+            final UserInfo userInfo,
+            final TokenDestinationItem tokenDestinationItem,
+            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 == null ? null : userInfo.getUserIdentity();
+        final Map<String, String> tokenMapData = new LinkedHashMap<>();
+
+        if ( userInfo != null )
+        {
+            final Instant userLastPasswordChange = userInfo.getPasswordLastModifiedTime();
+            if ( userLastPasswordChange != null )
+            {
+                final String userChangeString = JavaHelper.toIsoDate( userLastPasswordChange );
+                tokenMapData.put( PwmConstants.TOKEN_KEY_PWD_CHG_DATE, userChangeString );
+            }
+        }
+
+        if ( inputTokenData != null )
+        {
+            tokenMapData.putAll( inputTokenData );
+        }
+
+        final EmailItemBean emailItemBean = config.readSettingAsEmail( emailToSend, pwmRequest.getLocale() );
+        final String tokenKey;
+        final TokenPayload tokenPayload;
+        try
+        {
+            tokenPayload = pwmRequest.getPwmApplication().getTokenService().createTokenPayload(
+                    tokenType,
+                    new TimeDuration( config.readSettingAsLong( PwmSetting.TOKEN_LIFETIME ), TimeUnit.SECONDS ),
+                    tokenMapData,
+                    userIdentity,
+                    tokenDestinationItem.getValue()
+            );
+            tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
+        }
+        catch ( PwmOperationalException e )
+        {
+            throw new PwmUnrecoverableException( e.getErrorInformation() );
+        }
+
+        final String smsMessage = config.readSettingAsLocalizedString( smsToSend, pwmRequest.getLocale() );
+
+        TokenService.TokenSender.sendToken(
+                TokenService.TokenSendInfo.builder()
+                        .pwmApplication( pwmRequest.getPwmApplication() )
+                        .userInfo( userInfo )
+                        .macroMachine( macroMachine )
+                        .configuredEmailSetting( emailItemBean )
+                        .tokenSendMethod( tokenDestinationItem.getType().getMessageSendMethod() )
+                        .emailAddress( tokenDestinationItem.getValue() )
+                        .smsNumber( tokenDestinationItem.getValue() )
+                        .smsMessage( smsMessage )
+                        .tokenKey( tokenKey )
+                        .sessionLabel( pwmRequest.getSessionLabel() )
+                        .build()
+        );
+    }
+
+    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,

+ 13 - 32
server/src/main/java/password/pwm/ws/client/rest/RestTokenDataClient.java

@@ -23,6 +23,7 @@
 package password.pwm.ws.client.rest;
 
 import com.novell.ldapchai.exception.ChaiUnavailableException;
+import lombok.Value;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
@@ -35,7 +36,9 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
+import password.pwm.svc.token.TokenDestinationDisplayMasker;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 
@@ -49,41 +52,16 @@ public class RestTokenDataClient implements RestClient
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestTokenDataClient.class );
 
+    private final PwmApplication pwmApplication;
+
+    @Value
     public static class TokenDestinationData implements Serializable
     {
         private String email;
         private String sms;
         private String displayValue;
-
-        public TokenDestinationData(
-                final String email,
-                final String sms,
-                final String displayValue
-        )
-        {
-            this.email = email;
-            this.sms = sms;
-            this.displayValue = displayValue;
-        }
-
-        public String getEmail( )
-        {
-            return email;
-        }
-
-        public String getSms( )
-        {
-            return sms;
-        }
-
-        public String getDisplayValue( )
-        {
-            return displayValue;
-        }
     }
 
-    private final PwmApplication pwmApplication;
-
     public RestTokenDataClient( final PwmApplication pwmApplication )
     {
         this.pwmApplication = pwmApplication;
@@ -152,21 +130,24 @@ public class RestTokenDataClient implements RestClient
 
     private TokenDestinationData builtInService( final TokenDestinationData tokenDestinationData )
     {
+
+        final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( pwmApplication.getConfig() );
+
         final StringBuilder tokenSendDisplay = new StringBuilder();
 
-        if ( tokenDestinationData.getEmail() != null )
+        if ( !StringUtil.isEmpty( tokenDestinationData.getEmail() ) )
         {
-            tokenSendDisplay.append( tokenDestinationData.getEmail() );
+            tokenSendDisplay.append( tokenDestinationDisplayMasker.maskEmail( tokenDestinationData.getEmail() ) );
         }
 
-        if ( tokenDestinationData.getSms() != null )
+        if ( !StringUtil.isEmpty( tokenDestinationData.getSms() ) )
         {
             if ( tokenSendDisplay.length() > 0 )
             {
                 tokenSendDisplay.append( " / " );
             }
 
-            tokenSendDisplay.append( tokenDestinationData.getSms() );
+            tokenSendDisplay.append( tokenDestinationDisplayMasker.maskPhone( tokenDestinationData.getSms() ) );
         }
 
         return new TokenDestinationData(

+ 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()
         );

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

@@ -1748,6 +1748,11 @@
             <value><![CDATA[pwmToken]]></value>
         </default>
     </setting>
+    <setting hidden="false" key="token.valueMasking.enable" level="1" required="true">
+        <default>
+            <value>true</value>
+        </default>
+    </setting>
     <setting hidden="false" key="otp.enabled" level="1" required="true">
         <default>
             <value>false</value>
@@ -2962,6 +2967,7 @@
             <option value="NONE">None - Token verification will not be performed</option>
             <option value="EMAILONLY">Email - Send to email address</option>
             <option value="SMSONLY">SMS - Send via SMS</option>
+            <option value="CHOICE_SMS_EMAIL">User Choice - If both SMS and email address is available, user decides</option>
         </options>
     </setting>
     <setting hidden="false" key="updateAttributes.enable" level="1" required="true">
@@ -3012,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>-->

+ 12 - 13
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -87,6 +87,7 @@ Category_Description_OTP_SETTINGS=Options for time-based one time passwords.
 Category_Description_OTP_SETUP=Options for time-based one time passwords.
 Category_Description_PASSWORD_GLOBAL=Password related settings that apply to all users regardless of the password policy or profile appear here.  For profile-specific password settings, see Profiles -> Password Policy Profiles.
 Category_Description_PASSWORD_POLICY=Settings that define the LDAP directories that are used by the application.  If the user identities are in multiple LDAP directories, configure each directory as an LDAP Directory Profile.  Within each LDAP directory profile definition, you can control the individual servers and other settings for each LDAP directory.
+Category_Description_PW_EXP_NOTIFY=Password Expiration Notification
 Category_Description_PEOPLE_SEARCH=The people search module provides basic white pages or directory lookup functionality to your users.  Customizations allow easy searching and display quick detailed information about your users' colleagues.
 Category_Description_PROFILES=Profiles
 Category_Description_RECOVERY_DEF=Definition
@@ -187,6 +188,7 @@ Category_Label_PASSWORD_GLOBAL=Password Settings
 Category_Label_PASSWORD_POLICY=Password Policies
 Category_Label_PEOPLE_SEARCH=People Search
 Category_Label_PROFILES=Policies
+Category_Label_PW_EXP_NOTIFY=Password Expiration Notification
 Category_Label_RECOVERY_DEF=Definition
 Category_Label_RECOVERY=Forgotten Password
 Category_Label_RECOVERY_OAUTH=OAuth
@@ -600,6 +602,10 @@ Setting_Description_pwm.securityKey=<p>Specify a Security Key used for cryptogra
 Setting_Description_pwm.seedlist.location=Specify the location of the seed list in the form of a valid URL. When @PwmAppName@ randomly generates passwords, it can generate a "friendly", random password suggestions to users.  It does this by using a "seed" word or words, and then modifying that word randomly until it is sufficiently complex and meets the configured rules computed for the user.<br/><br/>The value must be a valid URL, using the protocol "file" (local file system), "http", or "https".
 Setting_Description_pwm.selfURL=<p>The URL to this application, as seen by users. @PwmAppName@ uses the value in email macros and other user-facing communications.</p><p>The URL must use a valid fully qualified hostname. Do not use a network address.</p><p>In simple environments, the URL will be the base of the URL in the browser you are currently using to view this page, however in more complex environments the URL will typically be an upstream proxy, gateway or network device.</p><p>The URL should include the path to the base application, typically <code>/@Case:lower:[[@PwmAppName@]]@</code>.</p>
 Setting_Description_pwm.wordlist.location=Specify a word list file URL for dictionary checking to prevent users from using commonly used words as passwords.   Using word lists is an important part of password security.  Word lists are used by intruders to guess common passwords.   The default word list included contains commonly used English passwords.  <br/><br/>The first time a startup occurs with a new word list setting, it takes some time to compile the word list into a database.  See the status screen and logs for progress information.  The word list file format is one or more text files containing a single word per line, enclosed in a ZIP file.  The String <i>\!\#comment\:</i> at the beginning of a line indicates a comment. <br/><br/>The value must be a valid URL, using the protocol "file" (local file system), "http", or "https".
+Setting_Description_pwNotify.enable=Enable Password Expiration Notification
+Setting_Description_pwNotify.queryString=Expiration Notification User Match
+Setting_Description_pwNotify.intervals=Expiration Notification Intervals
+Setting_Description_pwNotify.job.offSet=Job Offset
 Setting_Description_recovery.action=Add actions to take when the user completes the forgotten password process.
 Setting_Description_recovery.allowWhenLocked=Enable this option to allow users to use the forgotten password feature when the account is intruder locked in LDAP.  This feature is not available when a user is using NMAS stored responses.
 Setting_Description_recovery.bogus.user.enable=Enable this option to have forgotten password act as though invalid user searches are valid, and present such users with a bogus forgotten password policy.  This can help prevent username discovery.
@@ -684,6 +690,7 @@ Setting_Description_token.ldap.attribute=Specify the attribute that @PwmAppName@
 Setting_Description_token.length=Specify the length of the email token
 Setting_Description_token.lifetime=Specify the default lifetime an token is valid (in seconds). The default is one hour.  This default may be overridden by module specific settings.
 Setting_Description_token.storageMethod=Select the storage method @PwmAppName@ uses to save issued tokens.<table style\="width\: 400px"><tr><td>Method</td><td>Description</td></tr><tr><td>LocalDB</td><td>Stores the tokens in the local embedded LocalDB database.  Tokens are not common across multiple application instances.</td></tr><tr><td>DB</td><td>Store the tokens in a configured, remote database.  Tokens work across multiple application instances.</td></tr><tr><td>Crypto</td><td>Use crypto to create and read tokens, they are not stored locally.  Tokens work across multiple application instances if they have the same Security Key.  Crypto tokens ignore the length rules and might be too long to use for SMS purposes.</td></tr><tr><td>LDAP</td><td>Use the LDAP directory to store tokens.  Tokens work across multiple application instances.  You cannot use LDAP tokens as New User Registration tokens.</td></tr></table>
+Setting_Description_token.valueMasking.enable=Enable this option to mask token destination display values (email addresses and telephone numbers).
 Setting_Description_updateAttributes.check.queryMatch=When you use the "checkProfile" or "checkAll" parameter with the command servlet, @PwmAppName@ uses this query match to determine if the user is required to populate the parameter values. <br/><br/>If this value is blank, then @PwmAppName@ checks the user's current values against the form requirements.
 Setting_Description_updateAttributes.email.verification=Enable this option to send an email to the user's email address before @PwmAppName@ updates the account. The user's email must change to cause this verification email to be sent. The user must verify receipt of the email before @PwmAppName@ updates the account.
 Setting_Description_updateAttributes.enable=Enable the option to Update Profile Attributes.  If true, this setting enables the Update Profile module.
@@ -1091,6 +1098,10 @@ Setting_Label_pwm.securityKey=Security Key
 Setting_Label_pwm.seedlist.location=Seed List File URL
 Setting_Label_pwm.selfURL=Site URL
 Setting_Label_pwm.wordlist.location=Word List File URL
+Setting_Label_pwNotify.enable=Enable Password Expiration Notification
+Setting_Label_pwNotify.queryString=Expiration Notification User Match
+Setting_Label_pwNotify.intervals=Expiration Notification Intervals
+Setting_Label_pwNotify.job.offSet=Job Offset
 Setting_Label_recovery.action=Forgotten Password Recovery Mode
 Setting_Label_recovery.allowWhenLocked=Allow Forgotten Password when Locked
 Setting_Label_recovery.bogus.user.enable=Enable Bogus User Policy
@@ -1175,6 +1186,7 @@ Setting_Label_token.ldap.attribute=Token LDAP attribute name
 Setting_Label_token.length=Token Length
 Setting_Label_token.lifetime=Token Maximum Lifetime
 Setting_Label_token.storageMethod=Token Storage Method
+Setting_Label_token.valueMasking.enable=Enable Token Destination Value Masking
 Setting_Label_updateAttributes.check.queryMatch=Update Profile Check Match
 Setting_Label_updateAttributes.email.verification=Enable Email Verification
 Setting_Label_updateAttributes.enable=Enable Update Profile
@@ -1198,16 +1210,3 @@ Setting_Label_webservices.queryMatch=Web Services LDAP Authentication Permission
 Setting_Label_webservices.thirdParty.queryMatch=Web Services LDAP Third Party Permissions
 Setting_Label_webservice.userAttributes=Web Service User Attributes
 Setting_Label_wordlistCaseSensitive=Word List Case Sensitivity
-
-Category_Description_PW_EXP_NOTIFY=Password Expiration Notification
-Category_Label_PW_EXP_NOTIFY=Password Expiration Notification
-
-Setting_Label_pwNotify.enable=Enable Password Expiration Notification
-Setting_Label_pwNotify.queryString=Expiration Notification User Match
-Setting_Label_pwNotify.intervals=Expiration Notification Intervals
-Setting_Label_pwNotify.job.offSet=Job Offset
-
-Setting_Description_pwNotify.enable=Enable Password Expiration Notification
-Setting_Description_pwNotify.queryString=Expiration Notification User Match
-Setting_Description_pwNotify.intervals=Expiration Notification Intervals
-Setting_Description_pwNotify.job.offSet=Job Offset

+ 1 - 2
server/src/main/webapp/WEB-INF/jsp/activateuser-agreement.jsp

@@ -26,7 +26,6 @@
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
-<% final ActivateUserBean activateUserBean = JspUtility.getSessionBean(pageContext, ActivateUserBean.class); %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="fragment/header.jsp" %>
 <body class="nihilo">
@@ -38,7 +37,7 @@
     <div id="centerbody">
         <h1 id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
-        <% final String expandedText = activateUserBean.getAgreementText(); %>
+        <% final String expandedText = (String)JspUtility.getAttribute( pageContext, PwmRequestAttribute.AgreementText ); %>
         <br/><br/>
         <div id="agreementText" class="agreementText"><%= expandedText %></div>
         <div class="buttonbar">

+ 21 - 9
server/src/main/webapp/WEB-INF/jsp/activateuser-entercode.jsp

@@ -25,6 +25,7 @@
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ page import="password.pwm.http.bean.ActivateUserBean" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
+<%@ page import="password.pwm.http.servlet.activation.ActivateUserServlet" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <%@ include file="fragment/header.jsp" %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
@@ -36,7 +37,7 @@
     <div id="centerbody">
         <h1 id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></h1>
         <% final ActivateUserBean activateUserBean = JspUtility.getSessionBean(pageContext, ActivateUserBean.class); %>
-        <% final String destination = activateUserBean.getTokenDisplayText(); %>
+        <% final String destination = activateUserBean.getTokenDestination().getDisplay(); %>
         <p><pwm:display key="Display_RecoverEnterCode" value1="<%=destination%>"/></p>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <h2><label for="<%=PwmConstants.PARAM_TOKEN%>"><pwm:display key="Field_Code"/></label></h2>
@@ -50,8 +51,16 @@
                 <input type="hidden" id="processAction" name="processAction" value="enterCode"/>
                 <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
             </form>
+
+            <% if ( (Boolean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ShowGoBackButton) ) { %>
+            <button type="submit" id="button-goBack" name="button-goBack" class="btn" form="form-goBack">
+                <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-backward"></span></pwm:if>
+                <pwm:display key="Button_GoBack"/>
+            </button>
+            <% } %>
+
             <pwm:if test="<%=PwmIfTest.showCancel%>">
-                <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" class="something">
+                <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded">
                     <input type="hidden" name="processAction" value="reset"/>
                     <button type="submit" name="button" class="btn" id="buttonCancel">
                         <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span>&nbsp</pwm:if>
@@ -64,13 +73,16 @@
     </div>
     <div class="push"></div>
 </div>
-<pwm:script>
-<script type="text/javascript">
-    PWM_GLOBAL['startupFunctions'].push(function(){
-        PWM_MAIN.getObject('<%=PwmConstants.PARAM_TOKEN%>').focus();
-    });
-</script>
-</pwm:script>
+<form id="form-goBack" action="<pwm:current-url/>" method="post">
+    <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ActivateUserServlet.ActivateUserAction.reset%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ActivateUserServlet.ResetType.clearTokenDestination%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_FORM_ID%>" value="<pwm:FormID/>"/>
+</form>
+<form id="form-cancel" action="<pwm:current-url/>" method="post">
+    <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ActivateUserServlet.ActivateUserAction.reset%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ActivateUserServlet.ResetType.exitActivation%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_FORM_ID%>" value="<pwm:FormID/>"/>
+</form>
 <%@ include file="fragment/footer.jsp" %>
 </body>
 </html>

+ 117 - 0
server/src/main/webapp/WEB-INF/jsp/activateuser-tokenchoice.jsp

@@ -0,0 +1,117 @@
+<%--
+ ~ 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
+--%>
+
+<%@ page import="password.pwm.bean.TokenDestinationItem" %>
+<%@ page import="java.util.List" %>
+<%@ page import="password.pwm.http.servlet.activation.ActivateUserServlet" %>
+
+<!DOCTYPE html>
+<% List<TokenDestinationItem> tokenDestinationItems = (List)JspUtility.getAttribute(pageContext, PwmRequestAttribute.TokenDestItems ); %>
+<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
+<%@ taglib uri="pwm" prefix="pwm" %>
+<html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
+<%@ include file="fragment/header.jsp" %>
+<body class="nihilo">
+<div id="wrapper">
+    <jsp:include page="fragment/header-body.jsp">
+        <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
+    </jsp:include>
+    <div id="centerbody">
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
+        <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
+        <p><pwm:display key="Display_RecoverTokenSendChoices"/></p>
+        <table class="noborder">
+            <% for (final TokenDestinationItem item : tokenDestinationItems) { %>
+            <tr>
+                <td style="text-align: center">
+                    <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form">
+                        <button class="btn" type="submit" name="submitBtn">
+                            <% if (item.getType() == TokenDestinationItem.Type.email) { %>
+                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-file-text"></span></pwm:if>
+                            <pwm:display key="Button_Email"/>
+                            <% } else { %>
+                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-phone"></span></pwm:if>
+                            <pwm:display key="Button_SMS"/>
+                            <% } %>
+                        </button>
+                        <input type="hidden" name="choice" value="<%=item.getId()%>"/>
+                        <input type="hidden" name="processAction" value="<%=ActivateUserServlet.ActivateUserAction.tokenChoice%>"/>
+                        <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
+                    </form>
+                </td>
+                <td>
+                    <% if (item.getType() == TokenDestinationItem.Type.email) { %>
+                    <pwm:display key="Display_RecoverTokenSendChoiceEmail"/>
+                    <% } else { %>
+                    <pwm:display key="Display_RecoverTokenSendChoiceSMS"/>
+                    <% } %>
+                </td>
+            </tr>
+            <pwm:if test="<%=PwmIfTest.showMaskedTokenSelection%>">
+                <tr>
+                    <td>
+                    </td>
+                    <td>
+                        <%=item.getDisplay()%>
+                    </td>
+                </tr>
+                <tr>
+                    <td>
+                        &nbsp;
+                    </td>
+                </tr>
+            </pwm:if>
+            <% } %>
+        </table>
+        <div>
+            <div class="buttonbar">
+                <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
+                    <button type="submit" id="button-goBack" name="button-goBack" class="btn">
+                        <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-backward"></span></pwm:if>
+                        <pwm:display key="Button_GoBack"/>
+                    </button>
+                    <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ActivateUserServlet.ActivateUserAction.reset%>"/>
+                    <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ActivateUserServlet.ResetType.clearTokenDestination%>"/>
+                    <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
+                </form>
+                <pwm:if test="<%=PwmIfTest.showCancel%>">
+                    <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
+                        <button type="submit" name="button" class="btn" id="button-sendReset">
+                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
+                            <pwm:display key="Button_Cancel"/>
+                        </button>
+                        <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ActivateUserServlet.ActivateUserAction.reset%>"/>
+                        <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ActivateUserServlet.ResetType.exitActivation%>"/>
+                        <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
+                    </form>
+                </pwm:if>
+            </div>
+        </div>
+        </form>
+    </div>
+
+    <div class="push"></div>
+</div>
+<%@ include file="fragment/footer.jsp" %>
+</body>
+</html>
+

+ 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"/>

+ 2 - 2
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-tokenchoice.jsp

@@ -25,7 +25,7 @@
 <%@ page import="java.util.List" %>
 
 <!DOCTYPE html>
-<% List<TokenDestinationItem> tokenDestinationItems = (List)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordTokenDestItems); %>
+<% List<TokenDestinationItem> tokenDestinationItems = (List)JspUtility.getAttribute(pageContext, PwmRequestAttribute.TokenDestItems); %>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
@@ -84,8 +84,8 @@
         </table>
         <div>
         <div class="buttonbar">
+            <% if ("true".equals(JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordOptionalPageView))) { %>
             <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
-                <% if ("true".equals(JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordOptionalPageView))) { %>
                 <button type="submit" id="button-goBack" name="button-goBack" class="btn">
                     <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-backward"></span></pwm:if>
                     <pwm:display key="Button_GoBack"/>

+ 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>

+ 1 - 1
server/src/main/webapp/public/resources/js/configeditor-settings-challenges.js

@@ -296,7 +296,7 @@ ChallengeSettingHandler.editLocale = function(keyName, localeKey) {
                     };
                     processQuestion();
                     PWM_MAIN.addEventHandler(inputID, 'input', function () {
-                        if (!multiValues[rowKey]['adminDefined']) {
+                        if (multiValues[rowKey]['adminDefined']) {
                             PWM_VAR['clientSettingCache'][keyName][localeKey][rowKey]['text'] = PWM_MAIN.getObject(inputID).value;
                         }
                     });

+ 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 ------------------------------------