Browse Source

pw form field js/css refactoring and de-dojo in changepassword and newuser modules

Jason Rivard 2 years ago
parent
commit
aff15fd11f
31 changed files with 611 additions and 471 deletions
  1. 1 1
      lib-util/src/main/java/password/pwm/util/java/CollectionUtil.java
  2. 3 8
      lib-util/src/main/java/password/pwm/util/java/JavaHelper.java
  3. 28 0
      lib-util/src/test/java/password/pwm/util/java/JavaHelperTest.java
  4. 22 5
      server/src/main/java/password/pwm/PwmApplication.java
  5. 34 39
      server/src/main/java/password/pwm/PwmDomainUtil.java
  6. 2 1
      server/src/main/java/password/pwm/http/PwmRequestAttribute.java
  7. 1 1
      server/src/main/java/password/pwm/http/PwmRequestUtil.java
  8. 0 8
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  9. 1 1
      server/src/main/java/password/pwm/http/tag/CurrentUrlTag.java
  10. 71 0
      server/src/main/java/password/pwm/http/tag/PasswordChangeMessageTag.java
  11. 21 10
      server/src/main/java/password/pwm/http/tag/PasswordRequirementsTag.java
  12. 61 0
      server/src/main/java/password/pwm/http/tag/PwmTabIndexTag.java
  13. 2 3
      server/src/main/java/password/pwm/http/tag/conditional/PwmIfTag.java
  14. 7 2
      server/src/main/java/password/pwm/http/tag/conditional/PwmIfTest.java
  15. 2 0
      server/src/main/java/password/pwm/svc/wordlist/WordlistImporter.java
  16. 1 2
      server/src/main/java/password/pwm/util/secure/PwmTrustManager.java
  17. 3 1
      server/src/main/java/password/pwm/ws/client/rest/form/RestFormDataClient.java
  18. 1 1
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  19. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/changepassword-agreement.jsp
  20. 8 62
      webapp/src/main/webapp/WEB-INF/jsp/changepassword.jsp
  21. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/forgottenusername-complete.jsp
  22. 62 0
      webapp/src/main/webapp/WEB-INF/jsp/fragment/form-field-newpassword.jsp
  23. 21 87
      webapp/src/main/webapp/WEB-INF/jsp/fragment/form.jsp
  24. 3 4
      webapp/src/main/webapp/WEB-INF/jsp/newuser.jsp
  25. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/setupotpsecret-success.jsp
  26. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/updateprofile-confirm.jsp
  27. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/updateprofile.jsp
  28. 16 0
      webapp/src/main/webapp/WEB-INF/pwm-taglib.tld
  29. 129 144
      webapp/src/main/webapp/public/resources/js/changepassword.js
  30. 21 86
      webapp/src/main/webapp/public/resources/js/newuser.js
  31. 90 0
      webapp/src/main/webapp/public/resources/style.css

+ 1 - 1
lib-util/src/main/java/password/pwm/util/java/CollectionUtil.java

@@ -64,7 +64,7 @@ public final class CollectionUtil
 
         return input.stream()
                 .filter( Objects::nonNull )
-                .collect( Collectors.toUnmodifiableList() );
+                .toList();
     }
 
     public static <V> Set<V> stripNulls( final Set<V> input )

+ 3 - 8
lib-util/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -43,6 +43,7 @@ import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HexFormat;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -56,7 +57,7 @@ import java.util.zip.GZIPOutputStream;
 
 public final class JavaHelper
 {
-    private static final char[] HEX_CHAR_ARRAY = "0123456789ABCDEF".toCharArray();
+    private static final HexFormat HEX_FORMAT = HexFormat.of().withUpperCase();
 
     private JavaHelper()
     {
@@ -64,13 +65,7 @@ public final class JavaHelper
 
     public static String binaryArrayToHex( final byte[] buf )
     {
-        final char[] chars = new char[2 * buf.length];
-        for ( int i = 0; i < buf.length; ++i )
-        {
-            chars[2 * i] = HEX_CHAR_ARRAY[( buf[i] & 0xF0 ) >>> 4];
-            chars[2 * i + 1] = HEX_CHAR_ARRAY[buf[i] & 0x0F];
-        }
-        return new String( chars );
+        return HEX_FORMAT.formatHex( buf );
     }
 
     public static String throwableToString( final Throwable throwable )

+ 28 - 0
lib-util/src/test/java/password/pwm/util/java/JavaHelperTest.java

@@ -23,8 +23,36 @@ package password.pwm.util.java;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.stream.IntStream;
+
 public class JavaHelperTest
 {
+    @Test
+    public void binaryArrayToHexTest()
+    {
+        {
+            final byte[] bytes = {
+                    3,
+                    127,
+                    41,
+                    16,
+            };
+            Assertions.assertEquals( "037F2910", JavaHelper.binaryArrayToHex( bytes ) );
+        }
+
+        {
+
+            final byte[] bytes = new byte[128];
+            IntStream.range( 0, 127 ).forEach( value -> bytes[value] = (byte) value );
+            Assertions.assertEquals(
+                        """
+                        000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2\
+                        C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758\
+                        595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E00\
+                        """,
+                    JavaHelper.binaryArrayToHex( bytes ) );
+        }
+    }
 
     @Test
     public void concatByteArraysTwo()

+ 22 - 5
server/src/main/java/password/pwm/PwmApplication.java

@@ -90,9 +90,9 @@ public class PwmApplication
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmApplication.class );
 
-    static final int DOMAIN_STARTUP_THREADS = 10;
+    static final int DOMAIN_STARTUP_THREAD_COUNT = 10;
 
-    private volatile Map<DomainID, PwmDomain> domains = new HashMap<>();
+    private volatile Map<DomainID, PwmDomain> domains = Map.of();
     private String runtimeNonce = PwmApplicationUtil.makeRuntimeNonce();
 
     private final SessionLabel sessionLabel;
@@ -240,22 +240,39 @@ public class PwmApplication
         this.pwmEnvironment = pwmEnvironment;
         final AppConfig newConfig = this.pwmEnvironment.getConfig();
 
+        warnOnOlderNewConfig( oldConfig, newConfig );
+
         if ( !Objects.equals( oldConfig.getValueHash(), newConfig.getValueHash() ) )
         {
             processPwmAppRestart( );
         }
         else
         {
-            LOGGER.debug( sessionLabel, () -> "no system-level settings have been changed, restart of system services is not required" );
+            LOGGER.debug( sessionLabel, () -> "no system-level settings have been changed"
+                    + ", restart of system services is not required" );
         }
 
         PwmDomainUtil.reInitDomains( this, newConfig, oldConfig );
 
         runtimeNonce = PwmApplicationUtil.makeRuntimeNonce();
 
-        LOGGER.debug( sessionLabel, () -> "completed application restart with " + domains().size() + " domains", TimeDuration.fromCurrent( startTime ) );
+        LOGGER.debug( sessionLabel, () -> "completed application restart with " + domains().size()
+                + " domains", TimeDuration.fromCurrent( startTime ) );
     }
 
+    private void warnOnOlderNewConfig( final AppConfig oldConfig, final AppConfig newConfig )
+    {
+        final Instant newInstant = newConfig.getStoredConfiguration().modifyTime();
+        final Instant oldInstant = oldConfig.getStoredConfiguration().modifyTime();
+        if ( newInstant != null && oldInstant != null && newInstant.isBefore( oldInstant ) )
+        {
+            LOGGER.warn( sessionLabel, () -> " oldConfig (" + oldInstant + ") "
+                    + "is newer than new config (" + newInstant + ") by "
+                    + TimeDuration.between( oldInstant, newInstant ).asCompactString() );
+        }
+    }
+
+
     private void postInitTasks()
     {
         final Instant startTime = Instant.now();
@@ -382,7 +399,7 @@ public class PwmApplication
 
             final Instant startDomainShutdown = Instant.now();
             LOGGER.trace( sessionLabel, () -> "beginning shutdown of " + callables.size() + " running domains" );
-            pwmScheduler.executeImmediateThreadPerJobAndAwaitCompletion( DOMAIN_STARTUP_THREADS, callables, sessionLabel, PwmApplication.class );
+            pwmScheduler.executeImmediateThreadPerJobAndAwaitCompletion( DOMAIN_STARTUP_THREAD_COUNT, callables, sessionLabel, PwmApplication.class );
             LOGGER.trace( sessionLabel, () -> "shutdown of " + callables.size() + " running domains completed", TimeDuration.fromCurrent( startDomainShutdown ) );
         }
         catch ( final PwmUnrecoverableException e )

+ 34 - 39
server/src/main/java/password/pwm/PwmDomainUtil.java

@@ -26,6 +26,7 @@ import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
@@ -39,6 +40,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.Callable;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 class PwmDomainUtil
@@ -79,11 +81,15 @@ class PwmDomainUtil
         LOGGER.trace( pwmApplication.getSessionLabel(), () -> "beginning domain initializations" );
 
         final List<Callable<Optional<PwmUnrecoverableException>>> callables = domains.stream()
-                .map( DomainInitializingCallable::new )
-                .collect( Collectors.toUnmodifiableList() );
+                .map( PwmDomainUtil::makeCallableDomainInit )
+                .toList();
 
-        final  List<Optional<PwmUnrecoverableException>> domainStartException = pwmApplication.getPwmScheduler()
-                .executeImmediateThreadPerJobAndAwaitCompletion( PwmApplication.DOMAIN_STARTUP_THREADS, callables, pwmApplication.getSessionLabel(), PwmDomainUtil.class );
+        final List<Optional<PwmUnrecoverableException>> domainStartException = pwmApplication.getPwmScheduler()
+                .executeImmediateThreadPerJobAndAwaitCompletion(
+                        PwmApplication.DOMAIN_STARTUP_THREAD_COUNT,
+                        callables,
+                        pwmApplication.getSessionLabel(),
+                        PwmDomainUtil.class );
 
         final Optional<PwmUnrecoverableException> domainStartupException = domainStartException.stream()
                 .filter( Optional::isPresent )
@@ -99,17 +105,9 @@ class PwmDomainUtil
                 TimeDuration.fromCurrent( domainInitStartTime ) );
     }
 
-    private static class DomainInitializingCallable implements Callable<Optional<PwmUnrecoverableException>>
+    private static Callable<Optional<PwmUnrecoverableException>> makeCallableDomainInit( final PwmDomain pwmDomain )
     {
-        private final PwmDomain pwmDomain;
-
-        DomainInitializingCallable( final PwmDomain pwmDomain )
-        {
-            this.pwmDomain = pwmDomain;
-        }
-
-        @Override
-        public Optional<PwmUnrecoverableException> call()
+        return () ->
         {
             try
             {
@@ -120,7 +118,7 @@ class PwmDomainUtil
             {
                 return Optional.of( e );
             }
-        }
+        };
     }
 
     static void reInitDomains(
@@ -164,7 +162,8 @@ class PwmDomainUtil
 
         if ( newDomains.isEmpty() && deletedDomains.isEmpty() )
         {
-            LOGGER.debug( pwmApplication.getSessionLabel(), () -> "no domain-level settings have been changed, restart of domain services is not required" );
+            LOGGER.debug( pwmApplication.getSessionLabel(),
+                    () -> "no domain-level settings have been changed, restart of domain services is not required" );
         }
 
         if ( !newDomains.isEmpty() )
@@ -197,10 +196,22 @@ class PwmDomainUtil
 
     enum DomainModifyCategory
     {
-        removed,
-        unchanged,
-        modified,
-        created,
+        removed( new RemovalClassifier() ),
+        unchanged( new UnchangedClassifier() ),
+        modified( new ModifiedClassifier() ),
+        created( new CreationClassifier() ),;
+
+        private final DomainModificationClassifier classifier;
+
+        DomainModifyCategory( final DomainModificationClassifier classifier )
+        {
+            this.classifier = classifier;
+        }
+
+        public DomainModificationClassifier classifier()
+        {
+            return classifier;
+        }
     }
 
     public static Map<DomainModifyCategory, Set<DomainID>> categorizeDomainModifications(
@@ -208,24 +219,14 @@ class PwmDomainUtil
             final AppConfig oldConfig
     )
     {
-        {
-            final Instant newInstant = newConfig.getStoredConfiguration().modifyTime();
-            final Instant oldInstant = oldConfig.getStoredConfiguration().modifyTime();
-            if ( newInstant != null && oldInstant != null && newInstant.isBefore( oldInstant ) )
-            {
-                throw new IllegalStateException( "refusing request to categorize changes due to oldConfig "
-                        + "being newer than new config" );
-            }
-        }
-
         final Set<StoredConfigKey> modifiedValues = StoredConfigurationUtil.changedValues(
                 newConfig.getStoredConfiguration(),
                 oldConfig.getStoredConfiguration() );
 
-        return CLASSIFIERS.entrySet().stream()
+        return EnumUtil.enumStream( DomainModifyCategory.class )
                 .collect( Collectors.toUnmodifiableMap(
-                        Map.Entry::getKey,
-                        entry -> entry.getValue().categorize( newConfig, oldConfig, modifiedValues )
+                        Function.identity(),
+                        entry -> entry.classifier().categorize( newConfig, oldConfig, modifiedValues )
                 ) );
     }
 
@@ -234,12 +235,6 @@ class PwmDomainUtil
         Set<DomainID> categorize( AppConfig newConfig, AppConfig oldConfig, Set<StoredConfigKey> modifiedValues );
     }
 
-    private static final Map<DomainModifyCategory, DomainModificationClassifier> CLASSIFIERS = Map.of(
-            DomainModifyCategory.removed, new RemovalClassifier(),
-            DomainModifyCategory.created, new CreationClassifier(),
-            DomainModifyCategory.unchanged, new UnchangedClassifier(),
-            DomainModifyCategory.modified, new ModifiedClassifier() );
-
     private static class RemovalClassifier implements DomainModificationClassifier
     {
         @Override

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

@@ -79,7 +79,6 @@ public enum PwmRequestAttribute
 
     ChangePassword_MaxWaitSeconds,
     ChangePassword_CheckIntervalSeconds,
-    ChangePassword_PasswordPolicyChangeMessage,
 
     ForgottenPasswordChallengeSet,
     ForgottenPasswordOptionalPageView,
@@ -107,5 +106,7 @@ public enum PwmRequestAttribute
     ExternalResponsePrompts,
     ExternalResponseInstructions,
 
+    JspIndexTabCounter,
+
     GoBackAction,;
 }

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

@@ -143,7 +143,7 @@ public class PwmRequestUtil
         {
             final String xForwardedForValue = request.getHeader( HttpHeader.XForwardedFor.getHttpName() );
             if ( StringUtil.notEmpty( xForwardedForValue ) )
-            {
+            {           
                 Collections.addAll( candidateAddresses, xForwardedForValue.split( "," ) );
             }
         }

+ 0 - 8
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java

@@ -589,14 +589,6 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
 
     private void forwardToChangePage( final PwmRequest pwmRequest ) throws ServletException, PwmUnrecoverableException, IOException
     {
-        final Optional<String> passwordPolicyChangeMessage = pwmRequest.getPwmSession().getUserInfo().getPasswordPolicy().getChangeMessage( pwmRequest.getLocale() );
-        if ( passwordPolicyChangeMessage.isPresent() )
-        {
-            final MacroRequest macroRequest = pwmRequest.getMacroMachine( );
-            final String expandedMessage = macroRequest.expandMacros( passwordPolicyChangeMessage.get() );
-            pwmRequest.setAttribute( PwmRequestAttribute.ChangePassword_PasswordPolicyChangeMessage, expandedMessage );
-        }
-
         pwmRequest.forwardToJsp( JspUrl.PASSWORD_CHANGE );
     }
 }

+ 1 - 1
server/src/main/java/password/pwm/http/tag/CurrentUrlTag.java

@@ -52,7 +52,7 @@ public class CurrentUrlTag extends TagSupport
             {
                 /* ignore */
             }
-            LOGGER.error( () -> "error during CurrentUrl output: " + e.getMessage() );
+            LOGGER.error( () -> "error during CurrentUrl output: " + e.getMessage(), e );
         }
         return EVAL_PAGE;
     }

+ 71 - 0
server/src/main/java/password/pwm/http/tag/PasswordChangeMessageTag.java

@@ -0,0 +1,71 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.http.tag;
+
+import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.error.PwmException;
+import password.pwm.http.PwmRequest;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroRequest;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.jsp.JspTagException;
+import javax.servlet.jsp.tagext.TagSupport;
+import java.io.IOException;
+import java.util.Optional;
+
+/**
+ * @author Jason D. Rivard
+ */
+public class PasswordChangeMessageTag extends TagSupport
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordChangeMessageTag.class );
+
+
+    @Override
+    public int doEndTag( )
+            throws JspTagException
+    {
+        try
+        {
+            final PwmRequest pwmRequest = PwmRequest.forRequest( ( HttpServletRequest ) pageContext.getRequest(), ( HttpServletResponse ) pageContext.getResponse() );
+
+            final PwmPasswordPolicy pwmPasswordPolicy = PasswordRequirementsTag.readPasswordPolicy( pwmRequest );
+
+            final Optional<String> passwordPolicyChangeMessage = pwmPasswordPolicy.getChangeMessage( pwmRequest.getLocale() );
+
+            if ( passwordPolicyChangeMessage.isPresent() )
+            {
+                final MacroRequest macroRequest = pwmRequest.getMacroMachine( );
+                final String expandedMessage = macroRequest.expandMacros( passwordPolicyChangeMessage.get() );
+                pageContext.getOut().write( expandedMessage );
+            }
+        }
+        catch ( final IOException | PwmException e )
+        {
+            LOGGER.error( () -> "unexpected error during password change message generation: " + e.getMessage(), e );
+            throw new JspTagException( e.getMessage() );
+        }
+        return EVAL_PAGE;
+    }
+}
+

+ 21 - 10
server/src/main/java/password/pwm/http/tag/PasswordRequirementsTag.java

@@ -27,9 +27,12 @@ import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
+import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.http.servlet.newuser.NewUserServlet;
 import password.pwm.i18n.Display;
 import password.pwm.i18n.Message;
@@ -577,16 +580,7 @@ public class PasswordRequirementsTag extends TagSupport
 
             pwmRequest.getMacroMachine( );
 
-            final PwmPasswordPolicy passwordPolicy;
-            if ( getForm() != null && "newuser".equalsIgnoreCase( getForm() ) )
-            {
-                final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile( pwmRequest );
-                passwordPolicy = newUserProfile.getNewUserPasswordPolicy( pwmRequest.getPwmRequestContext() );
-            }
-            else
-            {
-                passwordPolicy = pwmSession.getUserInfo().getPasswordPolicy();
-            }
+            final PwmPasswordPolicy passwordPolicy = readPasswordPolicy( pwmRequest );
 
             final Optional<String> configuredRuleText = passwordPolicy.getRuleText( pwmRequest.getLocale() );
             if ( configuredRuleText.isPresent() )
@@ -619,5 +613,22 @@ public class PasswordRequirementsTag extends TagSupport
         }
         return EVAL_PAGE;
     }
+
+    static PwmPasswordPolicy readPasswordPolicy( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        if ( pwmRequest.isAuthenticated() )
+        {
+            return pwmRequest.getPwmSession().getUserInfo().getPasswordPolicy();
+        }
+
+        if ( pwmRequest.getURL().matches( PwmServletDefinition.NewUser ) )
+        {
+            final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile( pwmRequest );
+            return newUserProfile.getNewUserPasswordPolicy( pwmRequest.getPwmRequestContext() );
+        }
+
+        throw new PwmUnrecoverableException( PwmError.ERROR_INTERNAL, "password policy unavailable for requirements text generation" );
+    }
 }
 

+ 61 - 0
server/src/main/java/password/pwm/http/tag/PwmTabIndexTag.java

@@ -0,0 +1,61 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.http.tag;
+
+import password.pwm.http.JspUtility;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.servlet.jsp.tagext.TagSupport;
+import java.io.IOException;
+
+public class PwmTabIndexTag extends TagSupport
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmTabIndexTag.class );
+
+    @Override
+    public int doEndTag( )
+            throws javax.servlet.jsp.JspTagException
+    {
+        try
+        {
+            final PwmRequest pwmRequest = JspUtility.getPwmRequest( pageContext );
+            final Integer currentCounter = (Integer) pwmRequest.getAttribute( PwmRequestAttribute.JspIndexTabCounter );
+            final int nextCounter = currentCounter == null ? 1 : currentCounter + 1;
+            pageContext.getOut().write( String.valueOf( nextCounter ) );
+            pwmRequest.setAttribute( PwmRequestAttribute.JspIndexTabCounter, nextCounter );
+        }
+        catch ( final Exception e )
+        {
+            try
+            {
+                pageContext.getOut().write( "" );
+            }
+            catch ( final IOException e1 )
+            {
+                /* ignore */
+            }
+            LOGGER.error( () -> "error during PwmTabIndexTag output: " + e.getMessage(), e );
+        }
+        return EVAL_PAGE;
+    }
+}

+ 2 - 3
server/src/main/java/password/pwm/http/tag/conditional/PwmIfTag.java

@@ -24,7 +24,6 @@ import com.novell.ldapchai.exception.ChaiUnavailableException;
 import password.pwm.Permission;
 import password.pwm.PwmApplicationMode;
 import password.pwm.config.PwmSetting;
-import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestFlag;
 import password.pwm.http.servlet.resource.TextFileResource;
@@ -111,9 +110,9 @@ public class PwmIfTag extends BodyTagSupport
                         LOGGER.warn( pwmRequest, () -> errorMsg );
                     }
                 }
-                catch ( final PwmUnrecoverableException e )
+                catch ( final Exception e )
                 {
-                    LOGGER.error( () -> "error executing PwmIfTag for test '" + test + "', error: " + e.getMessage() );
+                    LOGGER.error( () -> "error executing PwmIfTag for test '" + test + "', error: " + e.getMessage(), e );
                 }
             }
         }

+ 7 - 2
server/src/main/java/password/pwm/http/tag/conditional/PwmIfTest.java

@@ -556,8 +556,13 @@ public enum PwmIfTest
         @Override
         public boolean test( final PwmRequest pwmRequest, final PwmIfOptions options ) throws PwmUnrecoverableException
         {
-            final ChangePasswordProfile changePasswordProfile = pwmRequest.getChangePasswordProfile();
-            return changePasswordProfile.readSettingAsBoolean( PwmSetting.PASSWORD_SHOW_AUTOGEN );
+            if ( pwmRequest.getURL().isChangePasswordURL() )
+            {
+                final ChangePasswordProfile changePasswordProfile = pwmRequest.getChangePasswordProfile();
+                return changePasswordProfile.readSettingAsBoolean( PwmSetting.PASSWORD_SHOW_AUTOGEN );
+            }
+
+            return false;
         }
     }
 

+ 2 - 0
server/src/main/java/password/pwm/svc/wordlist/WordlistImporter.java

@@ -314,6 +314,8 @@ class WordlistImporter implements Runnable
         {
             final String normalizedWord = wordType.convertInputFromWordlist( this.rootWordlist.getConfiguration(), input );
             incrementCharBufferCounter( Collections.singleton( normalizedWord ) );
+            importStatistics.update( StatKey.averageWordLength, normalizedWord.length() );
+            importStatistics.update( StatKey.chunksPerWord, 1 );
             bufferedWords.add( normalizedWord );
         }
     }

+ 1 - 2
server/src/main/java/password/pwm/util/secure/PwmTrustManager.java

@@ -36,7 +36,6 @@ import java.security.NoSuchProviderException;
 import java.security.SignatureException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
@@ -50,7 +49,7 @@ public class PwmTrustManager implements X509TrustManager
 
     private PwmTrustManager( final TrustManagerSettings trustManagerSettings, final List<X509Certificate> trustedCertificates )
     {
-        this.trustedCertificates = new ArrayList<>( trustedCertificates );
+        this.trustedCertificates = CollectionUtil.stripNulls( trustedCertificates );
         this.settings = trustManagerSettings;
     }
 

+ 3 - 1
server/src/main/java/password/pwm/ws/client/rest/form/RestFormDataClient.java

@@ -118,7 +118,9 @@ public class RestFormDataClient
             LOGGER.trace( () -> "external rest call returned: " + httpResponse.getStatusPhrase() + ", body: " + responseBody );
             if ( httpResponse.getStatusCode() != 200 )
             {
-                final String errorMsg = "received non-200 response code (" + httpResponse.getStatusCode() + ") when executing web-service";
+                final String errorMsg = "received non-200 response code (" + httpResponse.getStatusCode()
+                        + ") when executing form data client web-service call to "
+                        + remoteWebServiceConfiguration.getUrl();
                 LOGGER.error( () -> errorMsg );
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_UNREACHABLE, errorMsg ) );
             }

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

@@ -352,7 +352,7 @@ Setting_Description_display.updateAttributes.agreement=<p>Specify a message to d
 Setting_Description_domain.list=List of domains supported by this application instance.  Domain order is unimportant.  The value of the domain(s) may be used in public URLs and parameters.<p>Domains provide a way for multiple systems/sites/tenants/customers to use a single instance of this @PwmAppName@ application.  Typically only a single instance is required.  If multiple domains are listed, the configuration editor will allow per-domain configuration of many settings.  Other settings are system-level and apply to the entire application instance.</p><p>Saving the configuration after increasing or decreasing the number of domains beyond a single domain may cause application URLs to change, and this configuration editor will change to allow editing of multiple domain configurations</p>
 Setting_Description_domain.hosts=A list of explicit fully qualified DNS hostnames to be used for this domain.  If this application is accessed by a client using an exact hostname specified here, then this domain will be used to service the client.    Example: "password.acme.com".
 Setting_Description_domain.system.adminDomain=Administrative Domain
-Setting_Description_domain.system.domainPathsEnabled=<p>If enabled, domain IDs will be added to the URL path used to access this application, and URL paths will require the inclusion of the domain ID in the path.  Example: "/pwm/private/login" will become "/pwm/default/private/login" or "/pwm/acme/private/login".</p><p>Regardless of this setting, the domain is always accessible if the host header (the browser url) is matched by the setting in <code>@PwmSettingReference:domain.hosts@</code>.</p
+Setting_Description_domain.system.domainPathsEnabled=If enabled, domain IDs will be added to the URL path used to access this application, and URL paths will require the inclusion of the domain ID in the path.  Example: "/pwm/private/login" will become "/pwm/default/private/login" or "/pwm/acme/private/login".<br/><br/>Regardless of this setting, the domain is always accessible if the host header (the url shown in the browser address bar) is matched by the setting in <code>@PwmSettingReference:domain.hosts@</code>.
 Setting_Description_email.activation=Define this template to send an email to users after a successful activation.
 Setting_Description_email.activation.token=Define this template to send an email during the activation verification process. You can use %TOKEN% to insert the token value into the email.
 Setting_Description_email.adminAlert.toAddress=Define this template to send an email when System Audit events occur to the defined email addresses.

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/changepassword-agreement.jsp

@@ -46,7 +46,6 @@
         <h1><pwm:display key="Display_PasswordExpired"/></h1><br/>
         <% } %>
         <%@ include file="fragment/message.jsp" %>
-        <br/>
         <div id="agreementText" class="agreementText"><%= (String)JspUtility.getAttribute(pageContext, PwmRequestAttribute.AgreementText) %></div>
         <div class="buttonbar">
             <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" autocomplete="off">

+ 8 - 62
webapp/src/main/webapp/WEB-INF/jsp/changepassword.jsp

@@ -24,10 +24,8 @@
 
 
 <!DOCTYPE html>
-<%@ page import="password.pwm.PwmConstants" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ page import="password.pwm.http.tag.value.PwmValue" %>
-<%@ page import="password.pwm.http.PwmRequestAttribute" %>
 <%@ page import="password.pwm.http.servlet.changepw.ChangePasswordServlet" %>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -38,10 +36,10 @@
     <jsp:include page="fragment/header-body.jsp">
         <jsp:param name="pwm.PageName" value="Title_ChangePassword"/>
     </jsp:include>
-    <div id="centerbody" ng-app="changepassword.module" ng-controller="ChangePasswordController as $ctrl">
+    <div id="centerbody">
         <h1 id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></h1>
         <pwm:if test="<%=PwmIfTest.passwordExpired%>">
-        <h1><pwm:display key="Display_PasswordExpired"/></h1><br/>
+            <h1><pwm:display key="Display_PasswordExpired"/></h1><br/>
         </pwm:if>
         <p><pwm:display key="Display_ChangePassword"/></p>
         <div id="PasswordRequirements">
@@ -49,74 +47,24 @@
                 <pwm:DisplayPasswordRequirements separator="</li>" prepend="<li>"/>
             </ul>
         </div>
-        <%
-            final String passwordPolicyChangeMessage = (String)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ChangePassword_PasswordPolicyChangeMessage);
-        %>
-        <% if (passwordPolicyChangeMessage != null) { %>
-        <p><%= passwordPolicyChangeMessage %></p>
-        <% } %>
+        <div id="PasswordChangeMessage">
+            <p><pwm:PasswordChangeMessageTag/></p>
+        </div>
         <br/>
         <%@ include file="fragment/message.jsp" %>
-
         <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" id="changePasswordForm" autocomplete="off">
-            <table class="noborder">
-                <tr>
-                    <td class="noborder">
-                        <div style="width: 100%">
-                            <h2 style="display: inline">
-                                <label style="" for="password1"><pwm:display key="Field_NewPassword"/></label>
-                            </h2>
-                            &nbsp;&nbsp;
-                            <div class="pwm-icon pwm-icon-question-circle icon_button" id="password-guide-icon" style="cursor: pointer; visibility: hidden"></div>
-                            <pwm:if test="<%=PwmIfTest.showRandomPasswordGenerator%>">
-                                &nbsp;&nbsp;
-                                <div class="pwm-icon pwm-icon-retweet icon_button" id="autogenerate-icon" ng-click="$ctrl.doRandomGeneration()" style="cursor: pointer; visibility: hidden" ></div>
-                            </pwm:if>
-                        </div>
-                        <input type="<pwm:value name="<%=PwmValue.passwordFieldType%>"/>" name="password1" id="password1" class="changepasswordfield passwordfield" <pwm:autofocus/>/>
-                    </td>
-                    <td class="noborder" style="width:15%">
-                        <pwm:if test="<%=PwmIfTest.showStrengthMeter%>">
-                            <div id="strengthBox" style="visibility:hidden;">
-                                <div id="strengthLabel" style="padding-top:40px;">
-                                    <pwm:display key="Display_StrengthMeter"/>
-                                </div>
-                                <div class="progress-container" style="margin-bottom:10px">
-                                    <div id="strengthBar" style="width: 0">&nbsp;</div>
-                                </div>
-                            </div>
-                        </pwm:if>
-                    </td>
-                    <td class="noborder" style="width:10%">&nbsp;</td>
-                </tr>
-                <tr>
-                    <td class="noborder" style="width:75%">
-                        <h2 style="display: inline"><label for="password2"><pwm:display key="Field_ConfirmPassword"/></label></h2>
-                        <input type="<pwm:value name="<%=PwmValue.passwordFieldType%>"/>" name="password2" id="password2" class="changepasswordfield passwordfield"/>
-                    </td>
-                    <td class="noborder" style="width:15%">
-                        <%-- confirmation mark [not shown initially, enabled by javascript; see also changepassword.js:markConfirmationMark() --%>
-                        <div style="padding-top:45px;">
-                            <img style="visibility:hidden;" id="confirmCheckMark" alt="checkMark" height="15" width="15"
-                                 src="<pwm:context/><pwm:url url='/public/resources/greenCheck.png'/>">
-                            <img style="visibility:hidden;" id="confirmCrossMark" alt="crossMark" height="15" width="15"
-                                 src="<pwm:context/><pwm:url url='/public/resources/redX.png'/>">
-                        </div>
-                    </td>
-                    <td class="noborder" style="width:10%">&nbsp;</td>
-                </tr>
-            </table>
+            <jsp:include page="fragment/form-field-newpassword.jsp" />
 
             <input type="hidden" name="processAction" value="change"/>
             <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
 
             <div class="buttonbar" style="width:100%">
-                <button type="submit" name="password_button" class="btn" id="password_button">
+                <button type="submit" name="password_button" class="btn" id="password_button" tabindex="<pwm:tabindex/>">
                     <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-forward"></span></pwm:if>
                     <pwm:display key="Button_ChangePassword"/>
                 </button>
                 <pwm:if test="<%=PwmIfTest.passwordExpired%>" negate="true">
-                    <button id="button-reset" type="button" name="button-reset" class="btn" form="form-reset">
+                    <button id="button-reset" type="button" name="button-reset" class="btn" form="form-reset" tabindex="<pwm:tabindex/>">
                         <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
                         <pwm:display key="Button_Cancel"/>
                     </button>
@@ -139,8 +87,6 @@
 </pwm:script>
 
 <pwm:script-ref url="/public/resources/js/changepassword.js"/>
-<pwm:script-ref url="/public/resources/webjars/pwm-client/vendor.js" />
-<pwm:script-ref url="/public/resources/webjars/pwm-client/changepassword.ng.js" />
 
 <%@ include file="fragment/footer.jsp" %>
 </body>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/forgottenusername-complete.jsp

@@ -41,7 +41,6 @@
         <h1 id="page-content-title"><pwm:display key="Title_ForgottenUsername" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <% final String expandedText = (String) JspUtility.getAttribute(pageContext, PwmRequestAttribute.CompleteText); %>
-        <br/>
         <div id="agreementText" class="agreementText"><%= expandedText %></div>
         <div class="buttonbar">
             <form action="<pwm:url url='<%=PwmServletDefinition.PublicCommand.servletUrl()%>' addContext="true"/>" method="post" enctype="application/x-www-form-urlencoded" class="pwm-form">

+ 62 - 0
webapp/src/main/webapp/WEB-INF/jsp/fragment/form-field-newpassword.jsp

@@ -0,0 +1,62 @@
+<%--
+ ~ Password Management Servlets (PWM)
+ ~ http://www.pwm-project.org
+ ~
+ ~ Copyright (c) 2006-2009 Novell, Inc.
+ ~ Copyright (c) 2009-2021 The PWM Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~     http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+--%>
+<%--
+       THIS FILE IS NOT INTENDED FOR END USER MODIFICATION.
+       See the README.TXT file in WEB-INF/jsp before making changes.
+--%>
+
+<%@ taglib uri="pwm" prefix="pwm" %>
+
+<%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
+<%@ page import="password.pwm.http.tag.value.PwmValue" %>
+<div class="formFieldWrapper" id="formFieldWrapper-password1">
+    <div class="formFieldLabel">
+        <label for="password1"><pwm:display key="Field_NewPassword"/></label>
+        <div class="pwm-icon pwm-icon-question-circle pwm-icon-button nodisplay" id="password-guide-icon"></div>
+        <pwm:if test="<%=PwmIfTest.showRandomPasswordGenerator%>">
+            <div class="pwm-icon pwm-icon-retweet pwm-icon-button nodisplay" id="autogenerate-icon"></div>
+        </pwm:if>
+    </div>
+    <input type="<pwm:value name="<%=PwmValue.passwordFieldType%>"/>" name="password1" id="password1"
+           class="inputfield" <pwm:autofocus/> tabindex="<pwm:tabindex/>"/>
+    <pwm:if test="<%=PwmIfTest.showStrengthMeter%>">
+        <div id="strengthBox" class="noopacity">
+            <div id="strengthLabel">
+                <div class="pwm-icon pwm-icon-question-circle pwm-icon-button" id="strength-tooltip-icon"></div>
+                <div id="strengthLabelText"><pwm:display key="Display_StrengthMeter"/></div>
+            </div>
+            <progress id="passwordStrengthProgress" max="100" value="0"></progress>
+        </div>
+    </pwm:if>
+</div>
+<div class="formFieldWrapper" id="formFieldWrapper-password2">
+    <div class="formFieldLabel">
+        <label for="password2"><pwm:display key="Field_ConfirmPassword"/></label>
+    </div>
+    <input type="<pwm:value name="<%=PwmValue.passwordFieldType%>"/>" name="password2" id="password2"
+           class="inputfield" tabindex="<pwm:tabindex/>"/>
+    <%-- confirmation mark [not shown initially, enabled by javascript; see also changepassword.js:markConfirmationMark() --%>
+    <div id="confirmMarkBox">
+        <div class="confirmCheckMark nodisplay" id="confirmCheckMark"></div>
+        <div class="confirmCrossMark nodisplay" id="confirmCrossMark"
+             title="<pwm:display key="Password_DoesNotMatch" bundle="Error"/>"
+             alt="<pwm:display key="Password_DoesNotMatch" bundle="Error"/>"></div>
+    </div>
+</div>

+ 21 - 87
webapp/src/main/webapp/WEB-INF/jsp/fragment/form.jsp

@@ -66,30 +66,31 @@
 <div class="formFieldWrapper" id="formFieldWrapper-<%=loopConfiguration.getName()%>">
     <% if (loopConfiguration.getType().equals(FormConfiguration.Type.hidden)) { %>
     <input id="<%=loopConfiguration.getName()%>" type="hidden" class="inputfield"
-           name="<%=loopConfiguration.getName()%>" value="<%= currentValue %>"/>
+           name="<%=loopConfiguration.getName()%>" value="<%= currentValue %>"/> tabindex="<pwm:tabindex/>"
     <% } else if (loopConfiguration.getType().equals(FormConfiguration.Type.checkbox)) { %>
     <% final boolean checked = FormUtility.checkboxValueIsChecked(formDataMap.get(loopConfiguration)); %>
     <label class="checkboxWrapper">
-        <input id="<%=loopConfiguration.getName()%>" name="<%=loopConfiguration.getName()%>" type="checkbox" <%=checked?"checked":""%> <pwm:autofocus/>/>
+        <input id="<%=loopConfiguration.getName()%>" name="<%=loopConfiguration.getName()%>"
+               type="checkbox" <%=checked?"checked":""%> <pwm:autofocus/>/>
         <%=loopConfiguration.getLabel(formLocale)%>
         <%if(loopConfiguration.isRequired()){%>
-        <span style="font-style: italic; font-size: smaller" id="label_required_<%=loopConfiguration.getName()%>">*&nbsp;</span>
+        <span class="formFieldRequiredAsterisk" id="label_required_<%=loopConfiguration.getName()%>">*</span>
         <%}%>
     </label>
     <% if (loopConfiguration.getDescription(formLocale) != null && loopConfiguration.getDescription(formLocale).length() > 0) { %>
-    <p><%=loopConfiguration.getDescription(formLocale)%></p>
+    <div class="formFieldDescription"><%=loopConfiguration.getDescription(formLocale)%></div>
     <% } %>
     <% } else { %>
-    <label for="<%=loopConfiguration.getName()%>">
-        <div class="formFieldLabel">
+    <div class="formFieldLabel">
+        <label for="<%=loopConfiguration.getName()%>">
             <%= loopConfiguration.getLabel(formLocale) %>
             <%if(loopConfiguration.isRequired()){%>
-            <span style="font-style: italic; font-size: smaller" id="label_required_<%=loopConfiguration.getName()%>">*&nbsp;</span>
+            <span class="formFieldRequiredAsterisk" id="label_required_<%=loopConfiguration.getName()%>">*</span>
             <%}%>
-        </div>
-    </label>
+        </label>
+    </div>
     <% if (loopConfiguration.getDescription(formLocale) != null && loopConfiguration.getDescription(formLocale).length() > 0) { %>
-    <p><%=loopConfiguration.getDescription(formLocale)%></p>
+    <div class="formFieldDescription"><%=loopConfiguration.getDescription(formLocale)%></div>
     <% } %>
     <% final boolean readonly = loopConfiguration.isReadonly() || forceReadOnly; %>
     <% if (readonly && loopConfiguration.getType() != FormConfiguration.Type.photo) { %>
@@ -98,7 +99,8 @@
         <%= currentValue %>
         </span>
     <% } else if (loopConfiguration.getType() == FormConfiguration.Type.select) { %>
-    <select id="<%=loopConfiguration.getName()%>" name="<%=loopConfiguration.getName()%>" class="inputfield selectfield" <pwm:autofocus/> >
+    <select id="<%=loopConfiguration.getName()%>" name="<%=loopConfiguration.getName()%>" class="inputfield selectfield"
+            <pwm:autofocus/> tabindex="<pwm:tabindex/>">
         <% for (final String optionName : loopConfiguration.getSelectOptions().keySet()) {%>
         <option value="<%=optionName%>" <%if(optionName.equals(currentValue)){%>selected="selected"<%}%>>
             <%=loopConfiguration.getSelectOptions().get(optionName)%>
@@ -154,7 +156,7 @@
     </div>
     <% } else { %>
     <input id="<%=loopConfiguration.getName()%>" type="<%=loopConfiguration.getType()%>" class="inputfield"
-           name="<%=loopConfiguration.getName()%>" value="<%= currentValue %>"
+           name="<%=loopConfiguration.getName()%>" value="<%= currentValue %>" tabindex="<pwm:tabindex/>"
     <pwm:if test="<%=PwmIfTest.clientFormShowRegexEnabled%>">
             <%if (!StringUtil.isEmpty(loopConfiguration.getRegex())) {%> pattern="<%=loopConfiguration.getRegex()%>"<%}%>
     </pwm:if>
@@ -168,8 +170,8 @@
             <%if(loopConfiguration.isRequired()){%>*<%}%>
         </div>
     </label>
-    <input style="" id="<%=loopConfiguration.getName()%>_confirm" type="<%=loopConfiguration.getType()%>" class="inputfield"
-           name="<%=loopConfiguration.getName()%>_confirm"
+    <input id="<%=loopConfiguration.getName()%>_confirm" type="<%=loopConfiguration.getType()%>" class="inputfield"
+           name="<%=loopConfiguration.getName()%>_confirm" tabindex="<pwm:tabindex/>"
             <pwm:if test="<%=PwmIfTest.clientFormShowRegexEnabled%>">
                 <%if (!StringUtil.isEmpty(loopConfiguration.getRegex())) {%> pattern="<%=loopConfiguration.getRegex()%>"<%}%>
             </pwm:if>
@@ -229,83 +231,15 @@
     </pwm:script>
 </div>
 <% } %>
-
 <% if (showPasswordFields) { %>
-<h2>
-    <label for="password1"><pwm:display key="Field_NewPassword"/>
-        <span style="font-style: italic;font-size:smaller" id="label_required_password">*&nbsp;</span>
-        <pwm:script>
-            <script type="text/javascript">
-                PWM_GLOBAL['startupFunctions'].push(function(){
-                    PWM_MAIN.showTooltip({
-                        id: "label_required_password",
-                        text: '<%=PwmError.ERROR_FIELD_REQUIRED.getLocalizedMessage(formLocale,pwmDomain.getConfig(),new String[]{JspUtility.getMessage(pageContext,Display.Field_NewPassword)})%>',
-                        position: ['above']
-                    });
-                });
-            </script>
-        </pwm:script>
-    </label>
-</h2>
 <div id="PasswordRequirements">
     <ul>
-        <pwm:DisplayPasswordRequirements separator="</li>" prepend="<li>" form="newuser"/>
+        <pwm:DisplayPasswordRequirements separator="</li>" prepend="<li>"/>
     </ul>
 </div>
-<table class="noborder nomargin nopadding">
-    <tr class="noborder nomargin nopadding">
-        <td class="noborder nomargin nopadding" style="width:60%">
-            <input type="<pwm:value name="passwordFieldType"/>" name="password1" id="password1" class="changepasswordfield passwordfield" style="margin-left:5px"/>
-        </td>
-        <td class="noborder">
-            <pwm:if test="<%=PwmIfTest.showStrengthMeter%>">
-            <div id="strengthBox" style="visibility:hidden;">
-                <div id="strengthLabel">
-                    <pwm:display key="Display_StrengthMeter"/>
-                </div>
-                <div class="progress-container">
-                    <div id="strengthBar" style="width: 0">&nbsp;</div>
-                </div>
-            </div>
-            <pwm:script>
-                <script type="text/javascript">
-                    PWM_GLOBAL['startupFunctions'].push(function(){
-                        PWM_MAIN.showTooltip({
-                            id: ["strengthBox"],
-                            text: PWM_MAIN.showString('Tooltip_PasswordStrength'),
-                            width: 350
-                        });
-                    });
-                </script>
-            </pwm:script>
-            </pwm:if>
-        </td>
-        <td class="noborder" style="width:10%">&nbsp;</td>
-    </tr>
-    <tr class="noborder nomargin nopadding">
-        <td class="noborder nomargin nopadding">
-            <input type="<pwm:value name="<%=PwmValue.passwordFieldType%>"/>" name="password2" id="password2" class="changepasswordfield passwordfield" style="margin-left:5px"/>
-        </td>
-        <td class="noborder">
-            <%-- confirmation mark [not shown initially, enabled by javascript; see also changepassword.js:markConfirmationMark() --%>
-            <div style="padding-top:10px;">
-                <img style="visibility:hidden;" id="confirmCheckMark" alt="checkMark" height="15" width="15"
-                     src="<pwm:context/><pwm:url url='/public/resources/greenCheck.png'/>">
-                <img style="visibility:hidden;" id="confirmCrossMark" alt="crossMark" height="15" width="15"
-                     src="<pwm:context/><pwm:url url='/public/resources/redX.png'/>">
-            </div>
-        </td>
-        <td class="noborder" style="width:10%">&nbsp;</td>
-    </tr>
-</table>
-<pwm:script>
-    <script type="text/javascript">
-        PWM_GLOBAL['startupFunctions'].push(function(){
-            PWM_MAIN.addEventHandler('password1','keypress',function(){
-                PWM_MAIN.getObject('password2').value='';
-            });
-        });
-    </script>
-</pwm:script>
+<div id="PasswordChangeMessage">
+    <p><pwm:PasswordChangeMessageTag/></p>
+</div>
+<jsp:include page="form-field-newpassword.jsp" />
 <% } %>
 <% } %>

+ 3 - 4
webapp/src/main/webapp/WEB-INF/jsp/newuser.jsp

@@ -43,7 +43,6 @@
         <h1 id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_NewUser"/></p>
         <%@ include file="fragment/message.jsp" %>
-        <br/>
         <form action="<pwm:current-url/>" method="post" name="newUser" enctype="application/x-www-form-urlencoded" autocomplete="off"
               id="newUserForm" class="pwm-form pwm-form-captcha">
             <jsp:include page="fragment/form.jsp"/>
@@ -52,20 +51,20 @@
 
             <div class="buttonbar">
                 <input type="hidden" name="processAction" value="processForm"/>
-                <button type="submit" name="Create" class="btn pwm-btn-submit" id="submitBtn">
+                <button type="submit" name="Create" class="btn pwm-btn-submit" id="submitBtn" tabindex="<pwm:tabindex/>">
                     <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-forward"></span></pwm:if>
                     <pwm:display key="Button_Continue"/>
                 </button>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
 
                 <% if ((Boolean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.NewUser_FormShowBackButton)) { %>
-                <button type="button" id="button-goBack" name="button-goBack" class="btn" >
+                <button type="button" id="button-goBack" name="button-goBack" class="btn" tabindex="<pwm:tabindex/>">
                     <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%>">
-                    <button  type="button" id="button-cancel" name="button-cancel" class="btn">
+                    <button type="button" id="button-cancel" name="button-cancel" class="btn" tabindex="<pwm:tabindex/>">
                         <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
                         <pwm:display key="Button_Cancel"/>
                     </button>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/setupotpsecret-success.jsp

@@ -47,7 +47,6 @@
         <h1 id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></h1>
         <p><pwm:display key="Success_OtpSetup" bundle="Message"/></p>
         <%@ include file="fragment/message.jsp" %>
-        <br/>
         <% if (otpBean.getRecoveryCodes() != null && !otpBean.getRecoveryCodes().isEmpty()) { %>
         <table style="text-align: center">
             <tr>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/updateprofile-confirm.jsp

@@ -44,7 +44,6 @@
         <h1 id="page-content-title"><pwm:display key="Title_UpdateProfileConfirm" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_UpdateProfileConfirm"/></p>
         <%@ include file="fragment/message.jsp" %>
-        <br/>
         <% final Map<FormConfiguration,String> formDataMap = (Map<FormConfiguration,String>)JspUtility.getAttribute(pageContext, PwmRequestAttribute.FormData); %>
         <table id="ConfirmProfileTable">
             <% for (final Map.Entry<FormConfiguration, String> entry : formDataMap.entrySet()) { %>

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

@@ -41,7 +41,6 @@
         <br/>
         <p><pwm:display key="Display_UpdateProfile"/></p>
         <%@ include file="fragment/message.jsp" %>
-        <br/>
         <form action="<pwm:current-url/>" method="post" name="updateProfileForm" enctype="application/x-www-form-urlencoded" autocomplete="off"
               class="pwm-form" id="updateProfileForm">
 

+ 16 - 0
webapp/src/main/webapp/WEB-INF/pwm-taglib.tld

@@ -53,6 +53,16 @@
             <rtexprvalue>false</rtexprvalue>
         </attribute>
     </tag>
+    <tag>
+        <name>PasswordChangeMessageTag</name>
+        <tag-class>
+            password.pwm.http.tag.PasswordChangeMessageTag
+        </tag-class>
+        <body-content>empty</body-content>
+        <description>
+            Displays the configured password change message (if enabled)
+        </description>
+    </tag>
     <tag>
         <name>DisplayPasswordRequirements</name>
         <tag-class>
@@ -248,6 +258,12 @@
         <body-content>empty</body-content>
         <description>output the current page url</description>
     </tag>
+    <tag>
+        <name>tabindex</name>
+        <tag-class>password.pwm.http.tag.PwmTabIndexTag</tag-class>
+        <body-content>empty</body-content>
+        <description>output the next tabindex counter</description>
+    </tag>
     <tag>
         <name>throwableHandler</name>
         <tag-class>password.pwm.http.tag.JspThrowableHandlerTag</tag-class>

+ 129 - 144
webapp/src/main/webapp/public/resources/js/changepassword.js

@@ -24,9 +24,6 @@
 
 "use strict";
 
-var COLOR_BAR_TOP       = 0x8ced3f;
-var COLOR_BAR_BOTTOM    = 0xcc0e3e;
-
 var PWM_CHANGEPW = PWM_CHANGEPW || {};
 
 // takes password values in the password fields, sends an http request to the servlet
@@ -37,18 +34,19 @@ PWM_CHANGEPW.passwordConfirmField = "password2";
 
 PWM_CHANGEPW.validatePasswords = function(userDN, nextFunction)
 {
-    if (PWM_GLOBAL['previousP1'] !== PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value) {  // if p1 is changing, then clear out p2.
+    // if p1 is changing, then clear out p2.
+    if (PWM_GLOBAL['previousP1'] !== PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value) {
         PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField ).value = "";
         PWM_GLOBAL['previousP1'] = PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value;
     }
 
-    var validationProps = {};
+    const validationProps = {};
 
     validationProps['completeFunction'] = nextFunction ? nextFunction : function () {};
     validationProps['messageWorking'] = PWM_MAIN.showString('Display_CheckingPassword');
     validationProps['serviceURL'] = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction','checkPassword');
     validationProps['readDataFunction'] = function(){
-        var returnObj = {};
+        const returnObj = {};
         returnObj[PWM_CHANGEPW.passwordField ] = PWM_MAIN.getObject(PWM_CHANGEPW.passwordField ).value;
         returnObj[PWM_CHANGEPW.passwordConfirmField ] = PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField ).value;
         if (userDN) {
@@ -76,7 +74,7 @@ PWM_CHANGEPW.updateDisplay = function(resultInfo) {
         return;
     }
 
-    var message = resultInfo["message"];
+    const message = resultInfo["message"];
 
     if (resultInfo["version"] !== 2) {
         PWM_MAIN.showError("[ unexpected version string from server ]");
@@ -110,60 +108,49 @@ PWM_CHANGEPW.updateDisplay = function(resultInfo) {
 PWM_CHANGEPW.markConfirmationCheck = function(matchStatus) {
     if (PWM_MAIN.getObject("confirmCheckMark") && PWM_MAIN.getObject("confirmCrossMark")) {
         if (matchStatus === "MATCH") {
-            PWM_MAIN.getObject("confirmCheckMark").style.visibility = 'visible';
-            PWM_MAIN.getObject("confirmCrossMark").style.visibility = 'hidden';
-            PWM_MAIN.getObject("confirmCheckMark").width = '15';
-            PWM_MAIN.getObject("confirmCrossMark").width = '0';
+            PWM_MAIN.removeCssClass("confirmCheckMark","nodisplay")
+            PWM_MAIN.addCssClass("confirmCrossMark","nodisplay")
         } else if (matchStatus === "NO_MATCH") {
-            PWM_MAIN.getObject("confirmCheckMark").style.visibility = 'hidden';
-            PWM_MAIN.getObject("confirmCrossMark").style.visibility = 'visible';
-            PWM_MAIN.getObject("confirmCheckMark").width = '0';
-            PWM_MAIN.getObject("confirmCrossMark").width = '15';
+            PWM_MAIN.addCssClass("confirmCheckMark","nodisplay")
+            PWM_MAIN.removeCssClass("confirmCrossMark","nodisplay")
         } else {
-            PWM_MAIN.getObject("confirmCheckMark").style.visibility = 'hidden';
-            PWM_MAIN.getObject("confirmCrossMark").style.visibility = 'hidden';
-            PWM_MAIN.getObject("confirmCheckMark").width = '0';
-            PWM_MAIN.getObject("confirmCrossMark").width = '0';
+            PWM_MAIN.addCssClass("confirmCheckMark","nodisplay")
+            PWM_MAIN.addCssClass("confirmCrossMark","nodisplay")
         }
     }
 };
 
 PWM_CHANGEPW.markStrength = function(strength) { //strength meter
-    if (PWM_MAIN.getObject("strengthBox") === null) {
+    const passwordStrengthProgressElement = PWM_MAIN.getObject("passwordStrengthProgress");
+
+    if (!passwordStrengthProgressElement) {
         return;
     }
 
     if (PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value.length > 0) {
-        PWM_MAIN.getObject("strengthBox").style.visibility = 'visible';
-    } else {
-        PWM_MAIN.getObject("strengthBox").style.visibility = 'hidden';
+        PWM_MAIN.removeCssClass("strengthBox","noopacity");
     }
 
-    var strengthLabel = "";
-    var barColor = "";
+    let strengthDescr;
 
-    if (strength == 100) {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthVeryHigh');
+    if (strength >= 99) {
+        strengthDescr = "VeryHigh";
     } else if (strength >= 75) {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthHigh');
+        strengthDescr = "High";
     } else if (strength >= 45) {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthMedium');
+        strengthDescr = "Medium";
     } else if (strength >= 20) {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthLow');
+        strengthDescr = "Low";
     } else {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthVeryLow');
+        strengthDescr = "VeryLow";
     }
 
-    var colorFade = function(h1, h2, p) { return ((h1>>16)+((h2>>16)-(h1>>16))*p)<<16|(h1>>8&0xFF)+((h2>>8&0xFF)-(h1>>8&0xFF))*p<<8|(h1&0xFF)+((h2&0xFF)-(h1&0xFF))*p; }
-    var gradColor = colorFade(COLOR_BAR_BOTTOM, COLOR_BAR_TOP, strength / 100).toString(16) + '';
+    let strengthLabel = PWM_MAIN.showString('Display_PasswordStrength' + strengthDescr);
 
-    var barObject = PWM_MAIN.getObject("strengthBar");
-    if (barObject !== null) {
-        barObject.style.width = strength + '%';
-        barObject.style.backgroundColor = '#' + gradColor;
-    }
+    passwordStrengthProgressElement.value = strength;
+    passwordStrengthProgressElement.setAttribute("data-strength", strengthDescr);
 
-    var labelObject = PWM_MAIN.getObject("strengthLabel");
+    const labelObject = PWM_MAIN.getObject("strengthLabelText");
     if (labelObject !== null) {
         labelObject.innerHTML = strengthLabel === null ? "" : strengthLabel;
     }
@@ -199,18 +186,18 @@ PWM_CHANGEPW.handleChangePasswordSubmit=function(event) {
     console.log('intercepted change password submit');
     PWM_MAIN.cancelEvent(event);
 
-    var nextFunction = function(data) {
+    const nextFunction = function (data) {
         console.log('post change password submit handler');
         if (!data || data['data']['passed'] && 'MATCH' === data['data']['match']) {
             console.log('submitting password form');
             PWM_MAIN.getObject("changePasswordForm").submit();
         } else {
             PWM_MAIN.closeWaitDialog();
-            var match = data['data']['match'];
+            const match = data['data']['match'];
             if ('MATCH' !== match) {
                 PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField).value = '';
             }
-            var okFunction = function() {
+            const okFunction = function () {
                 if ('MATCH' === match || 'EMPTY' === match) {
                     PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).focus();
                 } else {
@@ -218,9 +205,9 @@ PWM_CHANGEPW.handleChangePasswordSubmit=function(event) {
                 }
                 PWM_CHANGEPW.validatePasswords();
             };
-            var title = PWM_MAIN.showString('Title_ChangePassword');
-            var message = '<div style="height:20px">' + data['data']['message'] + '.</div>';
-            PWM_MAIN.showDialog({text:message,title:title,okAction:okFunction});
+            const title = PWM_MAIN.showString('Title_ChangePassword');
+            const message = '<div style="height:20px">' + data['data']['message'] + '.</div>';
+            PWM_MAIN.showDialog({text: message, title: title, okAction: okFunction});
         }
     };
 
@@ -236,12 +223,12 @@ PWM_CHANGEPW.handleChangePasswordSubmit=function(event) {
 
 PWM_CHANGEPW.doRandomGeneration=function(randomConfig) {
     randomConfig = randomConfig === undefined ? {} : randomConfig;
-    var finishAction = 'finishAction' in randomConfig ? randomConfig['finishAction'] : function(password) {
+    const finishAction = 'finishAction' in randomConfig ? randomConfig['finishAction'] : function (password) {
         PWM_CHANGEPW.copyToPasswordFields(password)
     };
 
-    var eventHandlers = [];
-    var dialogBody = "";
+    const eventHandlers = [];
+    let dialogBody = "";
     if (randomConfig['dialog'] && randomConfig['dialog'].length > 0) {
         dialogBody += randomConfig['dialog'];
     } else {
@@ -250,19 +237,19 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) {
     dialogBody += "<br/><br/>";
     dialogBody += '<table class="noborder">';
 
-    for (var i = 0; i < 20; i++) {
+    for (let i = 0; i < 20; i++) {
         dialogBody += '<tr class="noborder">';
-        for (var j = 0; j < 2; j++) {
+        for (let j = 0; j < 2; j++) {
             i = i + j;
             (function(index) {
-                var elementID = "randomGen" + index;
-                dialogBody += '<td class="noborder" style="padding-bottom: 5px;" width="20%"><div style="visibility:hidden" class="link-randomPasswordValue" href="#" id="' + elementID + '">&nbsp;</div></td>';
+                const elementID = "randomGen" + index;
+                dialogBody += '<td class="noborder"><div class="link-randomPasswordValue"  id="' + elementID + '"></div></td>';
                 eventHandlers.push(function(){
                     PWM_MAIN.addEventHandler(elementID,'click',function(){
-                        var value = PWM_MAIN.getObject(elementID).innerHTML;
-                        var parser = new DOMParser();
-                        var dom = parser.parseFromString(value, 'text/html');
-                        var domString = dom.body.textContent;
+                        const value = PWM_MAIN.getObject(elementID).innerHTML;
+                        const parser = new DOMParser();
+                        const dom = parser.parseFromString(value, 'text/html');
+                        const domString = dom.body.textContent;
                         finishAction(domString);
                     });
                 });
@@ -289,8 +276,7 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) {
     });
 
 
-
-    var titleString = randomConfig['title'] === null ? PWM_MAIN.showString('Title_RandomPasswords') : randomConfig['title'];
+    const titleString = randomConfig['title'] ? randomConfig['title'] : PWM_MAIN.showString('Title_RandomPasswords');
     PWM_MAIN.showDialog({
         title:titleString,
         dialogClass:'narrow',
@@ -299,7 +285,7 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) {
         showClose:true,
         loadFunction:function(){
             PWM_CHANGEPW.beginFetchRandoms(randomConfig);
-            for (var i = 0; i < eventHandlers.length; i++) {
+            for (let i = 0; i < eventHandlers.length; i++) {
                 eventHandlers[i]();
             }
         }
@@ -308,8 +294,8 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) {
 
 PWM_CHANGEPW.beginFetchRandoms=function(randomConfig) {
     PWM_MAIN.getObject('moreRandomsButton').disabled = true;
-    var fetchList = new Array();
-    for (var counter = 0; counter < 20; counter++) {
+    const fetchList = new Array();
+    for (let counter = 0; counter < 20; counter++) {
         fetchList[counter] = 'randomGen' + counter;
     }
     fetchList.sort(function() {return 0.5 - Math.random()});
@@ -320,7 +306,7 @@ PWM_CHANGEPW.beginFetchRandoms=function(randomConfig) {
 
 PWM_CHANGEPW.fetchRandoms=function(randomConfig) {
     if (randomConfig['fetchList'].length < 1) {
-        var moreButton = PWM_MAIN.getObject('moreRandomsButton');
+        const moreButton = PWM_MAIN.getObject('moreRandomsButton');
         if (moreButton !== null) {
             moreButton.disabled = false;
             moreButton.focus();
@@ -329,18 +315,18 @@ PWM_CHANGEPW.fetchRandoms=function(randomConfig) {
     }
 
     if (randomConfig['fetchList'].length > 0) {
-        var successFunction = function(resultInfo) {
-            var password = resultInfo['data']["password"];
-            var elementID = randomConfig['fetchList'].pop();
-            var element = PWM_MAIN.getObject(elementID);
+        const successFunction = function (resultInfo) {
+            const password = resultInfo['data']["password"];
+            const elementID = randomConfig['fetchList'].pop();
+            const element = PWM_MAIN.getObject(elementID);
             if (element !== null) {
                 element.innerHTML = password;
             }
             PWM_CHANGEPW.fetchRandoms(randomConfig);
         };
 
-        var url = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction','randomPassword');
-        var content = randomConfig['dataInput'] === null ? { } : randomConfig['dataInput'];
+        const url = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction', 'randomPassword');
+        const content = randomConfig['dataInput'] === null ? {} : randomConfig['dataInput'];
 
         PWM_MAIN.ajaxRequest(url,successFunction,{content:content});
     }
@@ -352,7 +338,7 @@ PWM_CHANGEPW.startupChangePasswordPage=function() {
     PWM_CHANGEPW.markStrength(0);
 
     // add handlers for main form
-    var changePasswordForm = PWM_MAIN.getObject('changePasswordForm');
+    const changePasswordForm = PWM_MAIN.getObject('changePasswordForm');
     PWM_MAIN.addEventHandler(changePasswordForm,"keyup, change",function(){
         PWM_CHANGEPW.validatePasswords(null);
     });
@@ -368,12 +354,12 @@ PWM_CHANGEPW.startupChangePasswordPage=function() {
     });
 
     // show the auto generate password panel
-    var autoGenPasswordElement = PWM_MAIN.getObject("autogenerate-icon");
+    const autoGenPasswordElement = PWM_MAIN.getObject("autogenerate-icon");
     if (autoGenPasswordElement !== null) {
-        autoGenPasswordElement.style.visibility = 'visible';
-        // PWM_MAIN.addEventHandler(autoGenPasswordElement,'click',function(){
-        //     PWM_CHANGEPW.doRandomGeneration();
-        // });
+        PWM_MAIN.removeCssClass(autoGenPasswordElement, "nodisplay");
+        PWM_MAIN.addEventHandler(autoGenPasswordElement,'click',function(){
+            PWM_CHANGEPW.doRandomGeneration();
+        });
         PWM_MAIN.showTooltip({
             id: "autogenerate-icon",
             text: PWM_MAIN.showString('Display_AutoGeneratedPassword')
@@ -383,12 +369,12 @@ PWM_CHANGEPW.startupChangePasswordPage=function() {
     PWM_MAIN.addEventHandler('button-reset','click',function(event){
         console.log('intercepted reset button');
 
-        var p1Value = PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value;
-        var p2Value = PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField).value;
+        const p1Value = PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value;
+        const p2Value = PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField).value;
 
-        var submitForm = function(){
-            var resetForm = PWM_MAIN.getObject('form-reset');
-            PWM_MAIN.handleFormSubmit(resetForm );
+        const submitForm = function () {
+            const resetForm = PWM_MAIN.getObject('form-reset');
+            PWM_MAIN.handleFormSubmit(resetForm);
         };
 
         if ( p1Value.length > 0 || p2Value.length > 0 ) {
@@ -405,24 +391,17 @@ PWM_CHANGEPW.startupChangePasswordPage=function() {
         }
     });
 
-    var messageElement = PWM_MAIN.getObject("message");
+    const messageElement = PWM_MAIN.getObject("message");
     if (messageElement.firstChild.nodeValue.length < 2) {
         setTimeout(function(){
             PWM_MAIN.showInfo(PWM_MAIN.showString('Display_PasswordPrompt'));
         },100);
     }
 
-    PWM_MAIN.showDijitTooltip({
-        id: "strengthBox",
-        text: PWM_MAIN.showString('Tooltip_PasswordStrength'),
-        width: 350
-
-    });
-
     if (PWM_GLOBAL['passwordGuideText'] && PWM_GLOBAL['passwordGuideText'].length > 0) {
-        var iconElement = PWM_MAIN.getObject('password-guide-icon');
+        const iconElement = PWM_MAIN.getObject('password-guide-icon');
         if (iconElement) {
-            try {iconElement.style.visibility = 'visible';} catch (e) { /* noop */ }
+            PWM_MAIN.removeCssClass(iconElement,'nodisplay');
             PWM_MAIN.addEventHandler('password-guide-icon','click',function(){
                 PWM_CHANGEPW.showPasswordGuide();
             });
@@ -433,77 +412,83 @@ PWM_CHANGEPW.startupChangePasswordPage=function() {
         }
     }
 
+    const tooltipText = PWM_MAIN.showString('Tooltip_PasswordStrength');
+    if (tooltipText) {
+        const strengthHelpIcon = PWM_MAIN.getObject('strength-tooltip-icon');
+        if (strengthHelpIcon) {
+            PWM_MAIN.addEventHandler('strength-tooltip-icon','click',function(){
+                PWM_MAIN.showDialog({
+                    showClose:true,
+                    title: PWM_MAIN.showString('Title_PasswordGuide'),
+                    text: '<div id="passwordGuideTextContent">' + tooltipText + '</div>'
+                });
+            })
+        }
+    }
+
     setTimeout(function(){
         PWM_CHANGEPW.setInputFocus();
     },10);
 };
 
 PWM_CHANGEPW.setInputFocus=function() {
-    var currentPassword = PWM_MAIN.getObject('currentPassword');
+    const currentPassword = PWM_MAIN.getObject('currentPassword');
     if (currentPassword !== null) {
         setTimeout(function() { currentPassword.focus(); },10);
     } else {
-        var password1 = PWM_MAIN.getObject('password1');
+        const password1 = PWM_MAIN.getObject('password1');
         setTimeout(function() { password1.focus(); },10);
     }
 };
 
 PWM_CHANGEPW.refreshCreateStatus=function(refreshInterval) {
-    require(["dojo","dijit/registry"],function(dojo,registry){
-        var displayStringsUrl = PWM_MAIN.addParamToUrl(window.location.href, "processAction", "checkProgress");
-        var completedUrl = PWM_MAIN.addPwmFormIDtoURL(PWM_MAIN.addParamToUrl(window.location.href,  "processAction", "complete"));
-        var loadFunction = function(data) {
-            var supportsProgress = (document.createElement('progress').max !== undefined);
-            if (supportsProgress) {
-                console.log('beginning html5 progress refresh');
-                var html5passwordProgressBar = PWM_MAIN.getObject('html5ProgressBar');
-                dojo.setAttr(html5passwordProgressBar, "value", data['data']['percentComplete']);
-            } else {
-                console.log('beginning dojo progress refresh');
-                var progressBar = registry.byId('passwordProgressBar');
-                progressBar.set("value",data['data']['percentComplete']);
-            }
-
-            try {
-                var tableBody = '';
-                if (data['data']['messages']) {
-                    for (var msgItem in data['data']['messages']) {
-                        (function(message){
-                            if (message['show']) {
-                                tableBody += '<tr><td>' + message['label'] + '</td><td>';
-                                tableBody += message['complete'] ? PWM_MAIN.showString('Value_ProgressComplete') : PWM_MAIN.showString('Value_ProgressInProgress');
-                                tableBody += '</td></tr>';
-                            }
-                        }(data['data']['messages'][msgItem]));
-                    }
-                }
-                if (PWM_MAIN.getObject('progressMessageTable')) {
-                    PWM_MAIN.getObject('progressMessageTable').innerHTML = tableBody;
+    const displayStringsUrl = PWM_MAIN.addParamToUrl(window.location.href, "processAction", "checkProgress");
+    const completedUrl = PWM_MAIN.addPwmFormIDtoURL(PWM_MAIN.addParamToUrl(window.location.href, "processAction", "complete"));
+    const loadFunction = function (data) {
+        console.log('beginning html5 progress refresh');
+        const html5passwordProgressBar = PWM_MAIN.getObject('html5ProgressBar');
+        const percentComplete = data['data']['percentComplete'];
+        html5passwordProgressBar.setAttribute("value", percentComplete );
+
+        try {
+            let tableBody = '';
+            if (data['data']['messages']) {
+                for (let msgItem in data['data']['messages']) {
+                    (function (message) {
+                        if (message['show']) {
+                            tableBody += '<tr><td>' + message['label'] + '</td><td>';
+                            tableBody += message['complete'] ? PWM_MAIN.showString('Value_ProgressComplete') : PWM_MAIN.showString('Value_ProgressInProgress');
+                            tableBody += '</td></tr>';
+                        }
+                    }(data['data']['messages'][msgItem]));
                 }
-                if (PWM_MAIN.getObject('estimatedRemainingSeconds')) {
-                    PWM_MAIN.getObject('estimatedRemainingSeconds').innerHTML = data['data']['estimatedRemainingSeconds'];
-                }
-                if (PWM_MAIN.getObject('elapsedSeconds')) {
-                    PWM_MAIN.getObject('elapsedSeconds').innerHTML = data['data']['elapsedSeconds'];
-                }
-            } catch (e) {
-                console.log('unable to update progressMessageTable, error: ' + e);
             }
-
-            if (data['data']['complete'] === true) {
-                PWM_MAIN.gotoUrl(completedUrl,{delay:1000})
-            } else {
-                setTimeout(function(){
-                    PWM_CHANGEPW.refreshCreateStatus(refreshInterval);
-                },refreshInterval);
+            if (PWM_MAIN.getObject('progressMessageTable')) {
+                PWM_MAIN.getObject('progressMessageTable').innerHTML = tableBody;
             }
-        };
-        var errorFunction = function(error) {
-            console.log('unable to read password change status: ' + error);
-            setTimeout(function(){
+            if (PWM_MAIN.getObject('estimatedRemainingSeconds')) {
+                PWM_MAIN.getObject('estimatedRemainingSeconds').innerHTML = data['data']['estimatedRemainingSeconds'];
+            }
+            if (PWM_MAIN.getObject('elapsedSeconds')) {
+                PWM_MAIN.getObject('elapsedSeconds').innerHTML = data['data']['elapsedSeconds'];
+            }
+        } catch (e) {
+            console.log('unable to update progressMessageTable, error: ' + e);
+        }
+
+        if (data['data']['complete'] === true) {
+            PWM_MAIN.gotoUrl(completedUrl, {delay: 1000})
+        } else {
+            setTimeout(function () {
                 PWM_CHANGEPW.refreshCreateStatus(refreshInterval);
-            },refreshInterval);
-        };
-        PWM_MAIN.ajaxRequest(displayStringsUrl, loadFunction, {errorFunction:errorFunction});
-    });
+            }, refreshInterval);
+        }
+    };
+    const errorFunction = function (error) {
+        console.log('unable to read password change status: ' + error);
+        setTimeout(function () {
+            PWM_CHANGEPW.refreshCreateStatus(refreshInterval);
+        }, refreshInterval);
+    };
+    PWM_MAIN.ajaxRequest(displayStringsUrl, loadFunction, {errorFunction:errorFunction});
 };

+ 21 - 86
webapp/src/main/webapp/public/resources/js/newuser.js

@@ -18,15 +18,12 @@
  * limitations under the License.
  */
 
-var COLOR_BAR_TOP       = 0x8ced3f;
-var COLOR_BAR_BOTTOM    = 0xcc0e3e;
-
 var PWM_NEWUSER = PWM_NEWUSER || {};
 
 // takes response values in the fields, sends an http request to the servlet
 // and then parses (and displays) the response from the servlet.
 PWM_NEWUSER.validateNewUserForm=function() {
-    var validationProps = new Array();
+    const validationProps = [];
     validationProps['messageWorking'] = PWM_MAIN.showString('Display_CheckingData');
     validationProps['serviceURL'] = PWM_MAIN.addParamToUrl(window.location.href,"processAction","validate");
     validationProps['readDataFunction'] = function(){
@@ -40,12 +37,12 @@ PWM_NEWUSER.validateNewUserForm=function() {
 };
 
 PWM_NEWUSER.makeFormData=function() {
-    var paramData = { };
+    const paramData = {};
 
-    var newUserForm = PWM_MAIN.getObject('newUserForm');
+    const newUserForm = PWM_MAIN.getObject('newUserForm');
 
-    for (var i = 0; i < newUserForm.elements.length; i++ ) {
-        var loopElement = newUserForm.elements[i];
+    for (let i = 0; i < newUserForm.elements.length; i++ ) {
+        const loopElement = newUserForm.elements[i];
         paramData[loopElement.name] = loopElement.value;
     }
 
@@ -53,15 +50,15 @@ PWM_NEWUSER.makeFormData=function() {
 };
 
 PWM_NEWUSER.updateDisplay=function(data) {
-    PWM_NEWUSER.markConfirmationCheck(null);
-    PWM_NEWUSER.markStrength(null);
+    PWM_CHANGEPW.markConfirmationCheck(null);
+    PWM_CHANGEPW.markStrength(null);
 
     if (data['error']) {
         PWM_MAIN.showError(data['errorMessage']);
         PWM_MAIN.getObject("submitBtn").disabled = true;
     } else {
-        var resultInfo = data['data'];
-        var message = resultInfo["message"];
+        const resultInfo = data['data'];
+        const message = resultInfo["message"];
 
         if (resultInfo["passed"] === true) {
             if (resultInfo["match"] === "MATCH") {
@@ -76,96 +73,34 @@ PWM_NEWUSER.updateDisplay=function(data) {
             PWM_MAIN.showError(message);
         }
 
-        PWM_NEWUSER.markConfirmationCheck(resultInfo["match"]);
-        PWM_NEWUSER.markStrength(resultInfo["strength"]);
-    }
-};
-
-PWM_NEWUSER.markConfirmationCheck=function(matchStatus) {
-    if (PWM_MAIN.getObject("confirmCheckMark") || PWM_MAIN.getObject("confirmCrossMark")) {
-        if (matchStatus === "MATCH") {
-            PWM_MAIN.getObject("confirmCheckMark").style.visibility = 'visible';
-            PWM_MAIN.getObject("confirmCrossMark").style.visibility = 'hidden';
-            PWM_MAIN.getObject("confirmCheckMark").width = '15';
-            PWM_MAIN.getObject("confirmCrossMark").width = '0';
-        } else if (matchStatus === "NO_MATCH") {
-            PWM_MAIN.getObject("confirmCheckMark").style.visibility = 'hidden';
-            PWM_MAIN.getObject("confirmCrossMark").style.visibility = 'visible';
-            PWM_MAIN.getObject("confirmCheckMark").width = '0';
-            PWM_MAIN.getObject("confirmCrossMark").width = '15';
-        } else {
-            PWM_MAIN.getObject("confirmCheckMark").style.visibility = 'hidden';
-            PWM_MAIN.getObject("confirmCrossMark").style.visibility = 'hidden';
-            PWM_MAIN.getObject("confirmCheckMark").width = '0';
-            PWM_MAIN.getObject("confirmCrossMark").width = '0';
-        }
-    }
-};
-
-PWM_NEWUSER.markStrength=function(strength) { //strength meter
-    if (PWM_MAIN.getObject("strengthBox") === null) {
-        return;
-    }
-
-    if (PWM_MAIN.getObject("password1").value.length > 0) {
-        PWM_MAIN.getObject("strengthBox").style.visibility = 'visible';
-    } else {
-        PWM_MAIN.getObject("strengthBox").style.visibility = 'hidden';
-    }
-
-    var strengthLabel = "";
-    var barColor = "";
-
-    if (strength == 100) {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthVeryHigh');
-    } else if (strength >= 75) {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthHigh');
-    } else if (strength >= 45) {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthMedium');
-    } else if (strength >= 20) {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthLow');
-    } else {
-        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthVeryLow');
-    }
-
-    var colorFade = function(h1, h2, p) { return ((h1>>16)+((h2>>16)-(h1>>16))*p)<<16|(h1>>8&0xFF)+((h2>>8&0xFF)-(h1>>8&0xFF))*p<<8|(h1&0xFF)+((h2&0xFF)-(h1&0xFF))*p; }
-    var gradColor = colorFade(COLOR_BAR_BOTTOM, COLOR_BAR_TOP, strength / 100).toString(16) + '';
-
-    var barObject = PWM_MAIN.getObject("strengthBar");
-    if (barObject !== null && strength !== null) {
-        barObject.style.width = strength + '%';
-        barObject.style.backgroundColor = '#' + gradColor;
-    }
-
-    var labelObject = PWM_MAIN.getObject("strengthLabel");
-    if (labelObject !== null) {
-        labelObject.innerHTML = strengthLabel === null ? "" : strengthLabel;
+        PWM_CHANGEPW.markConfirmationCheck(resultInfo["match"]);
+        PWM_CHANGEPW.markStrength(resultInfo["strength"]);
     }
 };
 
 
 PWM_NEWUSER.refreshCreateStatus=function(refreshInterval) {
     require(["dojo","dijit/registry"],function(dojo,registry){
-        var checkStatusUrl = PWM_MAIN.addParamToUrl(window.location.pathname,"processAction","checkProgress");
-        var completedUrl = PWM_MAIN.addParamToUrl(window.location.pathname,"processAction","complete");
-        var loadFunction = function(data) {
-            var supportsProgress = (document.createElement('progress').max !== undefined);
+        const checkStatusUrl = PWM_MAIN.addParamToUrl(window.location.pathname, "processAction", "checkProgress");
+        const completedUrl = PWM_MAIN.addParamToUrl(window.location.pathname, "processAction", "complete");
+        const loadFunction = function (data) {
+            const supportsProgress = (document.createElement('progress').max !== undefined);
             if (supportsProgress) {
                 console.log('beginning html5 progress refresh');
-                var html5passwordProgressBar = PWM_MAIN.getObject('html5ProgressBar');
+                const html5passwordProgressBar = PWM_MAIN.getObject('html5ProgressBar');
                 dojo.setAttr(html5passwordProgressBar, "value", data['data']['percentComplete']);
             } else {
                 console.log('beginning dojo progress refresh');
-                var progressBar = registry.byId('passwordProgressBar');
-                progressBar.set("value",data['data']['percentComplete']);
+                const progressBar = registry.byId('passwordProgressBar');
+                progressBar.set("value", data['data']['percentComplete']);
             }
 
             if (data['data']['complete'] === true) {
-                PWM_MAIN.gotoUrl(completedUrl,{delay:1000})
+                PWM_MAIN.gotoUrl(completedUrl, {delay: 1000})
             } else {
-                setTimeout(function(){
+                setTimeout(function () {
                     PWM_NEWUSER.refreshCreateStatus(refreshInterval);
-                },refreshInterval);
+                }, refreshInterval);
             }
         };
         PWM_MAIN.ajaxRequest(checkStatusUrl, loadFunction, {method:'GET'});

+ 90 - 0
webapp/src/main/webapp/public/resources/style.css

@@ -212,6 +212,10 @@ html[dir="rtl"] table td.key {
     display: none;
 }
 
+.noopacity {
+    opacity: 0;
+}
+
 /* disable ie password reveal */
 input[type=password]::-ms-reveal {
     display: none;
@@ -954,6 +958,10 @@ input[type=search] {
     font-size: 13px;
 }
 
+.formFieldDescription {
+    margin: 5px;
+}
+
 .formFieldLabel {
     font-size: 13px;
     font-weight: bold;
@@ -1009,6 +1017,11 @@ input[type=search] {
     padding-bottom: 10px;
 }
 
+.formFieldRequiredAsterisk {
+    font-style: italic;
+    font-size: smaller;
+}
+
 .headerProgressBar {
     width: 600px;
     padding: 5px;
@@ -1563,8 +1576,62 @@ html[dir="rtl"] .message.message-error .errorDetail {
     display: inline-block;
 }
 
+.pwm-icon-button {
+    cursor: pointer;
+}
+
 #strengthLabel {
     white-space: nowrap;
+    margin: 0;
+}
+
+#strength-tooltip-icon {
+    display: inline-block;
+}
+
+#strengthLabelText {
+    display: inline-block;
+}
+
+#strengthBox {
+    display: inline-block;
+    vertical-align: top;
+    margin-left: 10px;
+    min-width: 140px;
+    transition: opacity 0.5s linear;
+}
+
+#passwordStrengthProgress {
+    display: block;
+    left: 0;
+    height: 12px;
+    width: 100px;
+    border: 1px solid #697374;
+}
+
+#passwordStrengthProgress::-webkit-progress-bar {
+    border-radius: 0;
+    background-color: white;
+}
+
+#passwordStrengthProgress::-webkit-progress-value {
+    border-radius: 0;
+    transition: all 1s;
+}
+#passwordStrengthProgress[data-strength="VeryLow"]::-webkit-progress-value{
+    background: linear-gradient(to right, rgba(200, 25, 62, .1), rgba(200, 25, 62, 1));
+}
+#passwordStrengthProgress[data-strength="Low"]::-webkit-progress-value{
+    background: linear-gradient(to right, rgba(177, 107, 62, .1), rgba(177, 107, 62, 1));
+}
+#passwordStrengthProgress[data-strength="Medium"]::-webkit-progress-value{
+    background: linear-gradient(to right, rgba(170, 124, 62, .1), rgba(170, 124, 62, 1));
+}
+#passwordStrengthProgress[data-strength="High"]::-webkit-progress-value{
+    background: linear-gradient(to right, rgba(163, 154, 62, .1), rgba(163, 154, 62, 1));
+}
+#passwordStrengthProgress[data-strength="VeryHigh"]::-webkit-progress-value{
+    background: linear-gradient(to right, rgba(140, 237, 63, .1), rgba(140, 237, 63, 1));
 }
 
 /*** TABS ****/
@@ -1788,3 +1855,26 @@ table.ias-table, table.ias-table td {
 .pre-whitespace {
     white-space: pre;
 }
+
+#confirmMarkBox {
+    display: inline-block;
+    vertical-align: top;
+    margin-left: 10px;
+    margin-top: 10px;
+    min-width: 140px;
+    transition: opacity 0.5s linear;
+}
+
+.confirmCheckMark {
+    background-image: url("greenCheck.png");
+    background-size: cover;
+    width: 15px;
+    height: 15px;
+}
+
+.confirmCrossMark {
+    background-image: url("redX.png");
+    background-size: cover;
+    width: 15px;
+    height: 15px;
+}