소스 검색

- macro refactoring
- improved macro unit tests
- switched to using sample data for macro help
- added macro for @User:PwExpireTime:pattern:tz@ for consistency

Jason Rivard 4 년 전
부모
커밋
c4ac86b500
80개의 변경된 파일2048개의 추가작업 그리고 1653개의 파일을 삭제
  1. 4 4
      server/src/main/java/password/pwm/PwmApplication.java
  2. 3 3
      server/src/main/java/password/pwm/bean/pub/PublicUserInfoBean.java
  3. 3 3
      server/src/main/java/password/pwm/config/LDAPPermissionInfo.java
  4. 5 5
      server/src/main/java/password/pwm/config/PwmSetting.java
  5. 2 2
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java
  6. 2 2
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  7. 3 3
      server/src/main/java/password/pwm/http/SessionManager.java
  8. 3 3
      server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  9. 5 5
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  10. 5 5
      server/src/main/java/password/pwm/http/servlet/DeleteAccountServlet.java
  11. 7 7
      server/src/main/java/password/pwm/http/servlet/ForgottenUsernameServlet.java
  12. 5 5
      server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  13. 3 3
      server/src/main/java/password/pwm/http/servlet/accountinfo/AccountInformationBean.java
  14. 5 5
      server/src/main/java/password/pwm/http/servlet/activation/ActivateUserUtils.java
  15. 3 3
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java
  16. 7 7
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  17. 3 3
      server/src/main/java/password/pwm/http/servlet/configeditor/CategoryInfo.java
  18. 5 13
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  19. 4 4
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServletUtils.java
  20. 3 3
      server/src/main/java/password/pwm/http/servlet/configeditor/SettingInfo.java
  21. 3 3
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  22. 3 3
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordStateMachine.java
  23. 3 3
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  24. 3 3
      server/src/main/java/password/pwm/http/servlet/forgottenpw/RemoteVerificationMethod.java
  25. 5 5
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskCardInfoBean.java
  26. 4 4
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java
  27. 6 6
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  28. 3 3
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServletUtil.java
  29. 5 5
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java
  30. 10 9
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  31. 3 3
      server/src/main/java/password/pwm/http/servlet/oauth/OAuthMachine.java
  32. 9 9
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  33. 3 3
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PhotoDataReader.java
  34. 3 3
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileServlet.java
  35. 8 8
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java
  36. 3 3
      server/src/main/java/password/pwm/http/tag/DisplayTag.java
  37. 3 3
      server/src/main/java/password/pwm/http/tag/ErrorMessageTag.java
  38. 7 7
      server/src/main/java/password/pwm/http/tag/PasswordRequirementsTag.java
  39. 3 3
      server/src/main/java/password/pwm/http/tag/PwmMacroTag.java
  40. 3 3
      server/src/main/java/password/pwm/http/tag/SuccessMessageTag.java
  41. 5 5
      server/src/main/java/password/pwm/http/tag/value/PwmValue.java
  42. 6 6
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  43. 3 3
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  44. 7 7
      server/src/main/java/password/pwm/svc/email/EmailServerUtil.java
  45. 11 11
      server/src/main/java/password/pwm/svc/email/EmailService.java
  46. 9 9
      server/src/main/java/password/pwm/svc/event/AuditRecordFactory.java
  47. 4 4
      server/src/main/java/password/pwm/svc/event/AuditService.java
  48. 3 3
      server/src/main/java/password/pwm/svc/event/CEFAuditFormatter.java
  49. 3 3
      server/src/main/java/password/pwm/svc/intruder/IntruderManager.java
  50. 3 3
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java
  51. 2 2
      server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java
  52. 4 4
      server/src/main/java/password/pwm/svc/token/TokenService.java
  53. 10 9
      server/src/main/java/password/pwm/svc/token/TokenUtil.java
  54. 2 2
      server/src/main/java/password/pwm/util/DailySummaryJob.java
  55. 3 3
      server/src/main/java/password/pwm/util/i18n/LocaleHelper.java
  56. 84 3
      server/src/main/java/password/pwm/util/macro/AbstractMacro.java
  57. 9 3
      server/src/main/java/password/pwm/util/macro/ExternalRestMacro.java
  58. 12 25
      server/src/main/java/password/pwm/util/macro/Macro.java
  59. 110 225
      server/src/main/java/password/pwm/util/macro/MacroMachine.java
  60. 26 0
      server/src/main/java/password/pwm/util/macro/MacroReplacer.java
  61. 227 0
      server/src/main/java/password/pwm/util/macro/MacroRequest.java
  62. 0 892
      server/src/main/java/password/pwm/util/macro/StandardMacros.java
  63. 59 71
      server/src/main/java/password/pwm/util/macro/StaticMacros.java
  64. 418 0
      server/src/main/java/password/pwm/util/macro/SystemMacros.java
  65. 480 0
      server/src/main/java/password/pwm/util/macro/UserMacros.java
  66. 16 16
      server/src/main/java/password/pwm/util/operations/ActionExecutor.java
  67. 3 3
      server/src/main/java/password/pwm/util/operations/OtpService.java
  68. 11 11
      server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java
  69. 12 11
      server/src/main/java/password/pwm/util/password/PasswordRuleReaderHelper.java
  70. 16 16
      server/src/main/java/password/pwm/util/password/PasswordUtility.java
  71. 3 3
      server/src/main/java/password/pwm/util/password/PwmPasswordRuleValidator.java
  72. 3 3
      server/src/main/java/password/pwm/ws/client/rest/RestTokenDataClient.java
  73. 3 3
      server/src/main/java/password/pwm/ws/server/rest/RestProfileServer.java
  74. 3 3
      server/src/main/java/password/pwm/ws/server/rest/RestStatusServer.java
  75. 52 53
      server/src/test/java/password/pwm/config/profile/PasswordRuleReaderHelperTest.java
  76. 227 40
      server/src/test/java/password/pwm/util/macro/MacroTest.java
  77. 2 2
      webapp/src/main/webapp/WEB-INF/jsp/configguide-start.jsp
  78. 3 3
      webapp/src/main/webapp/public/reference/settings.jsp
  79. 13 11
      webapp/src/main/webapp/public/resources/js/configeditor.js
  80. 11 5
      webapp/src/main/webapp/public/resources/text/macroHelp.html

+ 4 - 4
server/src/main/java/password/pwm/PwmApplication.java

@@ -83,7 +83,7 @@ import password.pwm.util.logging.LocalDBLogger;
 import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogManager;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.operations.CrService;
 import password.pwm.util.operations.OtpService;
 import password.pwm.util.queue.SmsQueueManager;
@@ -777,7 +777,7 @@ public class PwmApplication
             final String to,
             final String message,
             final SessionLabel sessionLabel,
-            final MacroMachine macroMachine
+            final MacroRequest macroRequest
     )
     {
         final SmsQueueManager smsQueue = getSmsQueue();
@@ -788,8 +788,8 @@ public class PwmApplication
         }
 
         final SmsItemBean smsItemBean = new SmsItemBean(
-                macroMachine.expandMacros( to ),
-                macroMachine.expandMacros( message ),
+                macroRequest.expandMacros( to ),
+                macroRequest.expandMacros( message ),
                 sessionLabel
         );
 

+ 3 - 3
server/src/main/java/password/pwm/bean/pub/PublicUserInfoBean.java

@@ -28,7 +28,7 @@ import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.tag.PasswordRequirementsTag;
 import password.pwm.ldap.UserInfo;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.io.Serializable;
 import java.time.Instant;
@@ -72,7 +72,7 @@ public class PublicUserInfoBean implements Serializable
             final UserInfo userInfoBean,
             final Configuration config,
             final Locale locale,
-            final MacroMachine macroMachine
+            final MacroRequest macroRequest
     )
             throws PwmUnrecoverableException
     {
@@ -110,7 +110,7 @@ public class PublicUserInfoBean implements Serializable
                 userInfoBean.getPasswordPolicy(),
                 config,
                 locale,
-                macroMachine
+                macroRequest
         );
 
         if ( userInfoBean.getCachedAttributeValues() != null && !userInfoBean.getCachedAttributeValues().isEmpty() )

+ 3 - 3
server/src/main/java/password/pwm/config/LDAPPermissionInfo.java

@@ -22,7 +22,7 @@ package password.pwm.config;
 
 import password.pwm.i18n.Config;
 import password.pwm.util.i18n.LocaleHelper;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.io.Serializable;
 import java.util.Locale;
@@ -69,8 +69,8 @@ public class LDAPPermissionInfo implements Serializable
 
         public String getDescription( final Locale locale, final Configuration config )
         {
-            final MacroMachine macroMachine = MacroMachine.forStatic();
-            return macroMachine.expandMacros( LocaleHelper.getLocalizedMessage( locale, "Actor_Description_" + this.toString(), config, Config.class ) );
+            final MacroRequest macroRequest = MacroRequest.forStatic();
+            return macroRequest.expandMacros( LocaleHelper.getLocalizedMessage( locale, "Actor_Description_" + this.toString(), config, Config.class ) );
         }
     }
 }

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

@@ -32,7 +32,7 @@ import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -1406,8 +1406,8 @@ public enum PwmSetting
     {
         final String propertyKey = password.pwm.i18n.PwmSetting.SETTING_DESCRIPTION_PREFIX + this.getKey();
         final String storedText = LocaleHelper.getLocalizedMessage( locale, propertyKey, null, password.pwm.i18n.PwmSetting.class );
-        final MacroMachine macroMachine = MacroMachine.forStatic();
-        return macroMachine.expandMacros( storedText );
+        final MacroRequest macroRequest = MacroRequest.forStatic();
+        return macroRequest.expandMacros( storedText );
     }
 
     public String getExample( final PwmSettingTemplateSet template )
@@ -1636,13 +1636,13 @@ static class PwmSettingReader
     private static List<TemplateSetReference<String>> readExamples( final PwmSetting pwmSetting )
     {
         final List<TemplateSetReference<String>> returnObj = new ArrayList<>();
-        final MacroMachine macroMachine = MacroMachine.forStatic();
+        final MacroRequest macroRequest = MacroRequest.forStatic();
         final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
         final List<XmlElement> exampleElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_EXAMPLE );
         for ( final XmlElement exampleElement : exampleElements )
         {
             final Set<PwmSettingTemplate> definedTemplates = PwmSettingXml.parseTemplateAttribute( exampleElement );
-            final String exampleString = macroMachine.expandMacros( exampleElement.getText() );
+            final String exampleString = macroRequest.expandMacros( exampleElement.getText() );
             returnObj.add( new TemplateSetReference<>( exampleString, Collections.unmodifiableSet( definedTemplates ) ) );
         }
         if ( returnObj.isEmpty() )

+ 2 - 2
server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java

@@ -45,7 +45,7 @@ import password.pwm.util.java.XmlDocument;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.secure.PwmSecurityKey;
 
 import java.io.IOException;
@@ -641,7 +641,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
         private static String generateCommentText()
         {
             final String resourceText = ResourceBundle.getBundle( StoredConfigurationFactory.class.getName() ).getString( "configCommentText" );
-            return MacroMachine.forStatic().expandMacros( resourceText );
+            return MacroRequest.forStatic().expandMacros( resourceText );
         }
     }
 

+ 2 - 2
server/src/main/java/password/pwm/health/LDAPHealthChecker.java

@@ -60,7 +60,7 @@ import password.pwm.util.java.JavaHelper;
 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.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.ws.server.rest.bean.HealthData;
@@ -976,7 +976,7 @@ public class LDAPHealthChecker implements HealthChecker
 
         try
         {
-            final String healthUsername = MacroMachine.forStatic().expandMacros( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_LDAP_USER_SEARCH_TERM ) );
+            final String healthUsername = MacroRequest.forStatic().expandMacros( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_LDAP_USER_SEARCH_TERM ) );
 
             final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
                     .enableValueEscaping( false )

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

@@ -46,7 +46,7 @@ import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.util.PasswordData;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.util.List;
 
@@ -244,13 +244,13 @@ public class SessionManager
         return status == Permission.PermissionStatus.GRANTED;
     }
 
-    public MacroMachine getMacroMachine( )
+    public MacroRequest getMacroMachine( )
             throws PwmUnrecoverableException
     {
         final UserInfo userInfoBean = pwmSession.isAuthenticated()
                 ? pwmSession.getUserInfo()
                 : null;
-        return MacroMachine.forUser( pwmApplication, pwmSession.getLabel(), userInfoBean, pwmSession.getLoginInfoBean() );
+        return MacroRequest.forUser( pwmApplication, pwmSession.getLabel(), userInfoBean, pwmSession.getLoginInfoBean() );
     }
 
     public Profile getProfile( final PwmApplication pwmApplication, final ProfileDefinition profileDefinition ) throws PwmUnrecoverableException

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

@@ -50,7 +50,7 @@ import password.pwm.util.i18n.LocaleHelper;
 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.macro.MacroRequest;
 import password.pwm.util.secure.PwmRandom;
 
 import javax.servlet.Filter;
@@ -359,7 +359,7 @@ public class RequestInitializationFilter implements Filter
             {
                 final String nonce = pwmRequest.getCspNonce();
                 final String replacedPolicy = contentPolicy.replace( "%NONCE%", nonce );
-                final String expandedPolicy = MacroMachine.forNonUserSpecific( pwmRequest.getPwmApplication(), null ).expandMacros( replacedPolicy );
+                final String expandedPolicy = MacroRequest.forNonUserSpecific( pwmRequest.getPwmApplication(), null ).expandMacros( replacedPolicy );
                 resp.setHeader( HttpHeader.ContentSecurityPolicy, expandedPolicy );
             }
         }
@@ -411,7 +411,7 @@ public class RequestInitializationFilter implements Filter
 
         if ( serverHeader != null && !serverHeader.isEmpty() )
         {
-            final String value = MacroMachine.forNonUserSpecific( pwmApplication, null ).expandMacros( serverHeader );
+            final String value = MacroRequest.forNonUserSpecific( pwmApplication, null ).expandMacros( serverHeader );
             resp.setHeader( HttpHeader.Server.getHttpName(), value );
         }
 

+ 5 - 5
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -50,7 +50,7 @@ import password.pwm.util.i18n.LocaleHelper;
 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.macro.MacroRequest;
 import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.SecureEngine;
 import password.pwm.ws.server.RestResultBean;
@@ -364,8 +364,8 @@ public class ClientApiServlet extends ControlledPwmServlet
                 );
                 if ( !StringUtil.isEmpty( configuredGuideText ) )
                 {
-                    final MacroMachine macroMachine = pwmSession.getSessionManager().getMacroMachine();
-                    final String expandedText = macroMachine.expandMacros( configuredGuideText );
+                    final MacroRequest macroRequest = pwmSession.getSessionManager().getMacroMachine();
+                    final String expandedText = macroRequest.expandMacros( configuredGuideText );
                     settingMap.put( "passwordGuideText", expandedText );
                 }
 
@@ -443,11 +443,11 @@ public class ClientApiServlet extends ControlledPwmServlet
         final ResourceBundle bundle = ResourceBundle.getBundle( displayClass.getName() );
         try
         {
-            final MacroMachine macroMachine = pwmSession.getSessionManager().getMacroMachine( );
+            final MacroRequest macroRequest = pwmSession.getSessionManager().getMacroMachine( );
             for ( final String key : new TreeSet<>( Collections.list( bundle.getKeys() ) ) )
             {
                 String displayValue = LocaleHelper.getLocalizedMessage( userLocale, key, config, displayClass );
-                displayValue = macroMachine.expandMacros( displayValue );
+                displayValue = macroRequest.expandMacros( displayValue );
                 displayStrings.put( key, displayValue );
             }
         }

+ 5 - 5
server/src/main/java/password/pwm/http/servlet/DeleteAccountServlet.java

@@ -46,7 +46,7 @@ import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 
 import javax.servlet.ServletException;
@@ -144,8 +144,8 @@ public class DeleteAccountServlet extends ControlledPwmServlet
         {
             if ( !bean.isAgreementPassed() )
             {
-                final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-                final String expandedText = macroMachine.expandMacros( selfDeleteAgreementText );
+                final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+                final String expandedText = macroRequest.expandMacros( selfDeleteAgreementText );
                 pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
                 pwmRequest.forwardToJsp( JspUrl.SELF_DELETE_AGREE );
                 return;
@@ -235,8 +235,8 @@ public class DeleteAccountServlet extends ControlledPwmServlet
         final String nextUrl = deleteAccountProfile.readSettingAsString( PwmSetting.DELETE_ACCOUNT_NEXT_URL );
         if ( nextUrl != null && !nextUrl.isEmpty() )
         {
-            final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-            final String macroedUrl = macroMachine.expandMacros( nextUrl );
+            final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+            final String macroedUrl = macroRequest.expandMacros( nextUrl );
             LOGGER.debug( pwmRequest, () -> "setting forward url to post-delete next url: " + macroedUrl );
             pwmRequest.getPwmSession().getSessionStateBean().setForwardURL( macroedUrl );
         }

+ 7 - 7
server/src/main/java/password/pwm/http/servlet/ForgottenUsernameServlet.java

@@ -48,7 +48,7 @@ import password.pwm.util.CaptchaUtility;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
@@ -327,9 +327,9 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet
             return new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg );
         }
 
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, userInfo, null );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, sessionLabel, userInfo, null );
 
-        pwmApplication.sendSmsUsingQueue( toNumber, smsMessage, sessionLabel, macroMachine );
+        pwmApplication.sendSmsUsingQueue( toNumber, smsMessage, sessionLabel, macroRequest );
         return null;
     }
 
@@ -347,9 +347,9 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet
             return new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg );
         }
 
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, userInfo, null );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, sessionLabel, userInfo, null );
 
-        pwmApplication.getEmailQueue().submitEmail( emailItemBean, userInfo, macroMachine );
+        pwmApplication.getEmailQueue().submitEmail( emailItemBean, userInfo, macroRequest );
 
         return null;
     }
@@ -366,8 +366,8 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet
     {
         final Locale locale = pwmRequest.getLocale();
         final String completeMessage = pwmRequest.getConfig().readSettingAsLocalizedString( PwmSetting.FORGOTTEN_USERNAME_MESSAGE, locale );
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getLabel(), userIdentity );
-        final String expandedText = macroMachine.expandMacros( completeMessage );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getLabel(), userIdentity );
+        final String expandedText = macroRequest.expandMacros( completeMessage );
         pwmRequest.setAttribute( PwmRequestAttribute.CompleteText, expandedText );
         pwmRequest.forwardToJsp( JspUrl.FORGOTTEN_USERNAME_COMPLETE );
     }

+ 5 - 5
server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java

@@ -58,7 +58,7 @@ import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.PwmDateFormat;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomPasswordGenerator;
@@ -492,12 +492,12 @@ public class GuestRegistrationServlet extends AbstractPwmServlet
                 final List<ActionConfiguration> actions = pwmApplication.getConfig().readSettingAsAction( PwmSetting.GUEST_WRITE_ATTRIBUTES );
                 if ( actions != null && !actions.isEmpty() )
                 {
-                    final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
+                    final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest, userIdentity );
 
 
                     final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, theUser )
                             .setExpandPwmMacros( true )
-                            .setMacroMachine( macroMachine )
+                            .setMacroMachine( macroRequest )
                             .createActionExecutor();
 
                     actionExecutor.executeActions( actions, pwmRequest.getLabel() );
@@ -615,9 +615,9 @@ public class GuestRegistrationServlet extends AbstractPwmServlet
             return;
         }
 
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest, userIdentity );
 
-        pwmApplication.getEmailQueue().submitEmail( configuredEmailSetting, null, macroMachine );
+        pwmApplication.getEmailQueue().submitEmail( configuredEmailSetting, null, macroRequest );
     }
 
     private void forwardToJSP(

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/accountinfo/AccountInformationBean.java

@@ -40,7 +40,7 @@ import password.pwm.util.form.FormUtility;
 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.macro.MacroRequest;
 
 import java.io.Serializable;
 import java.time.Instant;
@@ -106,8 +106,8 @@ public class AccountInformationBean implements Serializable
             throws PwmUnrecoverableException
     {
         final PwmPasswordPolicy pwmPasswordPolicy = pwmRequest.getPwmSession().getUserInfo().getPasswordPolicy();
-        final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
-        final List<String> rules = PasswordRequirementsTag.getPasswordRequirementsStrings( pwmPasswordPolicy, pwmRequest.getConfig(), pwmRequest.getLocale(), macroMachine );
+        final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
+        final List<String> rules = PasswordRequirementsTag.getPasswordRequirementsStrings( pwmPasswordPolicy, pwmRequest.getConfig(), pwmRequest.getLocale(), macroRequest );
         return Collections.unmodifiableList( rules );
     }
 

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

@@ -57,7 +57,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 
 import javax.servlet.ServletException;
@@ -109,11 +109,11 @@ class ActivateUserUtils
                 final List<ActionConfiguration> configValues = activateUserProfile.readSettingAsAction( PwmSetting.ACTIVATE_USER_PRE_WRITE_ATTRIBUTES );
                 if ( !JavaHelper.isEmpty( configValues ) )
                 {
-                    final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
+                    final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest, userIdentity );
 
                     final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
                             .setExpandPwmMacros( true )
-                            .setMacroMachine( macroMachine )
+                            .setMacroMachine( macroRequest )
                             .createActionExecutor();
 
                     actionExecutor.executeActions( configValues, pwmRequest.getLabel() );
@@ -323,8 +323,8 @@ class ActivateUserUtils
                 pwmRequest.getLocale()
         );
 
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, ActivateUserServlet.userInfo( pwmRequest ).getUserIdentity() );
-        final String expandedText = macroMachine.expandMacros( agreementText );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest, ActivateUserServlet.userInfo( pwmRequest ).getUserIdentity() );
+        final String expandedText = macroRequest.expandMacros( agreementText );
         pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
         pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_AGREEMENT );
     }

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java

@@ -39,7 +39,7 @@ import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.pwnotify.PwNotifyUserStatus;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 
 import java.util.Collections;
@@ -89,13 +89,13 @@ public class UserDebugDataReader
             /* disregard */
         }
 
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, locale, sessionLabel, userIdentity );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, locale, sessionLabel, userIdentity );
 
         final PwNotifyUserStatus pwNotifyUserStatus = readPwNotifyUserStatus( pwmApplication, userIdentity, sessionLabel );
 
         return UserDebugDataBean.builder()
                 .userInfo( userInfo )
-                .publicUserInfoBean( PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroMachine ) )
+                .publicUserInfoBean( PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroRequest ) )
                 .permissions( permissions )
                 .profiles( profiles )
                 .ldapPasswordPolicy( ldapPasswordPolicy )

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

@@ -58,7 +58,7 @@ 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.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.PwmPasswordRuleValidator;
 import password.pwm.util.password.RandomPasswordGenerator;
@@ -386,8 +386,8 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
             pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, ChangePasswordBean.class );
             if ( completeMessage != null && !completeMessage.isEmpty() )
             {
-                final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-                final String expandedText = macroMachine.expandMacros( completeMessage );
+                final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+                final String expandedText = macroRequest.expandMacros( completeMessage );
                 pwmRequest.setAttribute( PwmRequestAttribute.CompleteText, expandedText );
                 pwmRequest.forwardToJsp( JspUrl.PASSWORD_COMPLETE );
             }
@@ -473,8 +473,8 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
         final String agreementMsg = changePasswordProfile.readSettingAsLocalizedString( PwmSetting.PASSWORD_CHANGE_AGREEMENT_MESSAGE, pwmRequest.getLocale() );
         if ( !StringUtil.isEmpty( agreementMsg ) && !changePasswordBean.isAgreementPassed() )
         {
-            final MacroMachine macroMachine = pwmSession.getSessionManager().getMacroMachine();
-            final String expandedText = macroMachine.expandMacros( agreementMsg );
+            final MacroRequest macroRequest = pwmSession.getSessionManager().getMacroMachine();
+            final String expandedText = macroRequest.expandMacros( agreementMsg );
             pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
             pwmRequest.forwardToJsp( JspUrl.PASSWORD_AGREEMENT );
             return;
@@ -571,8 +571,8 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
         final String passwordPolicyChangeMessage = pwmRequest.getPwmSession().getUserInfo().getPasswordPolicy().getRuleHelper().getChangeMessage();
         if ( passwordPolicyChangeMessage.length() > 1 )
         {
-            final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-            macroMachine.expandMacros( passwordPolicyChangeMessage );
+            final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+            macroRequest.expandMacros( passwordPolicyChangeMessage );
             pwmRequest.setAttribute( PwmRequestAttribute.ChangePassword_PasswordPolicyChangeMessage, passwordPolicyChangeMessage );
         }
 

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/configeditor/CategoryInfo.java

@@ -22,7 +22,7 @@ package password.pwm.http.servlet.configeditor;
 
 import lombok.Data;
 import password.pwm.config.PwmSettingCategory;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.io.Serializable;
 import java.util.Locale;
@@ -42,13 +42,13 @@ public class CategoryInfo implements Serializable
 
     public static CategoryInfo forCategory(
             final PwmSettingCategory category,
-            final MacroMachine macroMachine,
+            final MacroRequest macroRequest,
             final Locale locale )
     {
         final CategoryInfo categoryInfo = new CategoryInfo();
         categoryInfo.key = category.getKey();
         categoryInfo.level = category.getLevel();
-        categoryInfo.description = macroMachine.expandMacros( category.getDescription( locale ) );
+        categoryInfo.description = macroRequest.expandMacros( category.getDescription( locale ) );
         categoryInfo.label = category.getLabel( locale );
         categoryInfo.hidden = category.isHidden();
         if ( category.getParent() != null )

+ 5 - 13
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -75,7 +75,7 @@ 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.macro.MacroRequest;
 import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.queue.SmsQueueManager;
 import password.pwm.util.secure.HttpsServerCertificateManager;
@@ -676,11 +676,11 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             final Optional<EmailServer> emailServer = EmailServerUtil.makeEmailServer( testConfiguration, emailServerProfile, null );
             if ( emailServer.isPresent() )
             {
-                final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, pwmRequest.getUserInfoIfLoggedIn() );
+                final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest, pwmRequest.getUserInfoIfLoggedIn() );
 
                 try
                 {
-                    EmailService.sendEmailSynchronous( emailServer.get(), testConfiguration, testEmailItem, macroMachine );
+                    EmailService.sendEmailSynchronous( emailServer.get(), testConfiguration, testEmailItem, macroRequest );
                     returnRecords.add( new HealthRecord( HealthStatus.INFO, HealthTopic.Email, "message sent" ) );
                 }
                 catch ( final MessagingException | PwmException e )
@@ -884,17 +884,9 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 return ProcessStatus.Halt;
             }
 
-            final MacroMachine macroMachine;
-            if ( pwmRequest.isAuthenticated() )
-            {
-                macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
-            }
-            else
-            {
-                macroMachine = MacroMachine.forNonUserSpecific( pwmRequest.getPwmApplication(), pwmRequest.getLabel() );
-            }
+            final MacroRequest macroRequest = MacroRequest.sampleMacroRequest( pwmRequest.getPwmApplication() );
             final String input = inputMap.get( "input" );
-            final String output = macroMachine.expandMacros( input );
+            final String output = macroRequest.expandMacros( input );
             pwmRequest.outputJsonResult( RestResultBean.withData( output ) );
         }
         catch ( final PwmUnrecoverableException e )

+ 4 - 4
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServletUtils.java

@@ -51,7 +51,7 @@ import password.pwm.i18n.PwmLocaleBundle;
 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.macro.MacroRequest;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.bean.HealthData;
 
@@ -208,7 +208,7 @@ public class ConfigEditorServletUtils
             throws PwmUnrecoverableException
     {
         final Instant startTime = Instant.now();
-        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, sessionLabel );
+        final MacroRequest macroRequest = MacroRequest.forNonUserSpecific( pwmApplication, sessionLabel );
         final PwmSettingTemplateSet template = storedConfiguration.getTemplateSet();
         final SettingData.SettingDataBuilder builder = SettingData.builder();
 
@@ -217,7 +217,7 @@ public class ConfigEditorServletUtils
             for ( final PwmSetting setting : PwmSetting.values() )
             {
 
-                settingMap.put( setting.getKey(), SettingInfo.forSetting( setting, template, macroMachine, locale ) );
+                settingMap.put( setting.getKey(), SettingInfo.forSetting( setting, template, macroRequest, locale ) );
             }
             builder.settings( settingMap );
         }
@@ -225,7 +225,7 @@ public class ConfigEditorServletUtils
             final LinkedHashMap<String, Object> categoryMap = new LinkedHashMap<>();
             for ( final PwmSettingCategory category : PwmSettingCategory.values() )
             {
-                categoryMap.put( category.getKey(), CategoryInfo.forCategory( category, macroMachine, locale ) );
+                categoryMap.put( category.getKey(), CategoryInfo.forCategory( category, macroRequest, locale ) );
             }
             builder.categories( categoryMap );
         }

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/configeditor/SettingInfo.java

@@ -27,7 +27,7 @@ import password.pwm.config.PwmSettingFlag;
 import password.pwm.config.PwmSettingProperty;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.PwmSettingTemplateSet;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -55,13 +55,13 @@ public class SettingInfo implements Serializable
     static SettingInfo forSetting(
             final PwmSetting setting,
             final PwmSettingTemplateSet template,
-            final MacroMachine macroMachine,
+            final MacroRequest macroRequest,
             final Locale locale
     )
     {
         final SettingInfo settingInfo = new SettingInfo();
         settingInfo.key = setting.getKey();
-        settingInfo.description = macroMachine.expandMacros( setting.getDescription( locale ) );
+        settingInfo.description = macroRequest.expandMacros( setting.getDescription( locale ) );
         settingInfo.level = setting.getLevel();
         settingInfo.label = setting.getLabel( locale );
         settingInfo.syntax = setting.getSyntax();

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

@@ -85,7 +85,7 @@ 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.macro.MacroRequest;
 import password.pwm.util.operations.cr.NMASCrOperator;
 import password.pwm.util.operations.otp.OTPUserRecord;
 import password.pwm.util.password.PasswordUtility;
@@ -1004,8 +1004,8 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         final String agreementMsg = forgottenPasswordProfile.readSettingAsLocalizedString( PwmSetting.RECOVERY_AGREEMENT_MESSAGE, pwmRequest.getLocale() );
         if ( !StringUtil.isEmpty( agreementMsg ) && !forgottenPasswordBean.isAgreementPassed() )
         {
-            final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
-            final String expandedText = macroMachine.expandMacros( agreementMsg );
+            final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
+            final String expandedText = macroRequest.expandMacros( agreementMsg );
             pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
             pwmRequest.forwardToJsp( JspUrl.RECOVER_USER_AGREEMENT );
             return;

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordStateMachine.java

@@ -74,7 +74,7 @@ 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.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.operations.otp.OTPUserRecord;
 import password.pwm.ws.server.PresentableForm;
@@ -307,7 +307,7 @@ public class ForgottenPasswordStateMachine
             final Locale locale = forgottenPasswordStateMachine.getCommonValues().getLocale();
             final UserIdentity userIdentity = forgottenPasswordStateMachine.getForgottenPasswordBean().getUserIdentity();
             final UserInfo userInfo = UserInfoFactory.newUserInfoUsingProxy( commonValues, userIdentity );
-            final MacroMachine macroMachine = MacroMachine.forUser( commonValues, userIdentity );
+            final MacroRequest macroRequest = MacroRequest.forUser( commonValues, userIdentity );
             final PwmPasswordPolicy pwmPasswordPolicy = userInfo.getPasswordPolicy();
 
             final boolean valueMasking = commonValues.getConfig().readSettingAsBoolean( PwmSetting.DISPLAY_MASK_PASSWORD_FIELDS );
@@ -333,7 +333,7 @@ public class ForgottenPasswordStateMachine
                     pwmPasswordPolicy,
                     commonValues.getConfig(),
                     commonValues.getLocale(),
-                    macroMachine );
+                    macroRequest );
 
             final String ruleDelimiter = commonValues.getConfig().readAppProperty( AppProperty.REST_SERVER_FORGOTTEN_PW_RULE_DELIMITER );
             final String ruleText = StringUtil.collectionToString( passwordRequirementsList, ruleDelimiter );

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

@@ -70,7 +70,7 @@ import password.pwm.util.PasswordData;
 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.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomPasswordGenerator;
 
@@ -204,7 +204,7 @@ public class ForgottenPasswordUtil
         }
 
         final UserInfo userInfo = readUserInfo( commonValues, forgottenPasswordBean );
-        final MacroMachine macroMachine = MacroMachine.forUser(
+        final MacroRequest macroRequest = MacroRequest.forUser(
                 pwmApplication,
                 commonValues.getSessionLabel(),
                 userInfo,
@@ -214,7 +214,7 @@ public class ForgottenPasswordUtil
         pwmApplication.getEmailQueue().submitEmail(
                 configuredEmailSetting,
                 userInfo,
-                macroMachine
+                macroRequest
         );
     }
 

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/forgottenpw/RemoteVerificationMethod.java

@@ -42,7 +42,7 @@ import password.pwm.svc.httpclient.PwmHttpClientResponse;
 import password.pwm.ldap.UserInfo;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -142,11 +142,11 @@ public class RemoteVerificationMethod implements VerificationMethodSystem
         headers.put( HttpHeader.ContentType.getHttpName(), HttpContentType.json.getHeaderValueWithEncoding() );
         headers.put( HttpHeader.AcceptLanguage.getHttpName(), locale.toLanguageTag() );
 
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, userInfo, null );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, sessionLabel, userInfo, null );
 
         final RemoteVerificationRequestBean remoteVerificationRequestBean = RemoteVerificationRequestBean.builder()
             .responseSessionID( this.remoteSessionID )
-            .userInfo( PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroMachine ) )
+            .userInfo( PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroRequest ) )
             .userResponses( userResponses )
             .build();
 

+ 5 - 5
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskCardInfoBean.java

@@ -37,7 +37,7 @@ import password.pwm.ldap.UserInfoFactory;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.io.Serializable;
 import java.time.Instant;
@@ -102,13 +102,13 @@ public class HelpdeskCardInfoBean implements Serializable
 
     static String figureDisplayName(
             final HelpdeskProfile helpdeskProfile,
-            final MacroMachine macroMachine
+            final MacroRequest macroRequest
     )
     {
         final String configuredDisplayName = helpdeskProfile.readSettingAsString( PwmSetting.HELPDESK_DETAIL_DISPLAY_NAME );
         if ( configuredDisplayName != null && !configuredDisplayName.isEmpty() )
         {
-            return macroMachine.expandMacros( configuredDisplayName );
+            return macroRequest.expandMacros( configuredDisplayName );
         }
         return null;
     }
@@ -125,10 +125,10 @@ public class HelpdeskCardInfoBean implements Serializable
         final List<String> displayStringSettings = helpdeskProfile.readSettingAsStringArray( PwmSetting.HELPDESK_DISPLAY_NAMES_CARD_LABELS );
         if ( displayStringSettings != null )
         {
-            final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, userInfo, null );
+            final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, sessionLabel, userInfo, null );
             for ( final String displayStringSetting : displayStringSettings )
             {
-                final String displayLabel = macroMachine.expandMacros( displayStringSetting );
+                final String displayLabel = macroRequest.expandMacros( displayStringSetting );
                 displayLabels.add( displayLabel );
             }
         }

+ 4 - 4
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java

@@ -52,7 +52,7 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.io.Serializable;
 import java.time.Instant;
@@ -128,7 +128,7 @@ public class HelpdeskDetailInfoBean implements Serializable
                 userIdentity,
                 theUser.getChaiProvider()
         );
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), userInfo, null );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), userInfo, null );
 
         try
         {
@@ -159,7 +159,7 @@ public class HelpdeskDetailInfoBean implements Serializable
                     userInfo.getPasswordPolicy(),
                     pwmRequest.getConfig(),
                     pwmRequest.getLocale(),
-                    macroMachine
+                    macroRequest
             );
             builder.passwordRequirements( Collections.unmodifiableList( requirementLines ) );
         }
@@ -207,7 +207,7 @@ public class HelpdeskDetailInfoBean implements Serializable
 
         }
 
-        builder.userDisplayName( HelpdeskCardInfoBean.figureDisplayName( helpdeskProfile, macroMachine ) );
+        builder.userDisplayName( HelpdeskCardInfoBean.figureDisplayName( helpdeskProfile, macroRequest ) );
 
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
 

+ 6 - 6
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -81,7 +81,7 @@ 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.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.CrService;
 import password.pwm.util.operations.OtpService;
@@ -281,10 +281,10 @@ public class HelpdeskServlet extends ControlledPwmServlet
             final ChaiUser chaiUser = useProxy
                     ? pwmRequest.getPwmApplication().getProxiedChaiUser( userIdentity )
                     : pwmRequest.getPwmSession().getSessionManager().getActor( userIdentity );
-            final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
+            final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest, userIdentity );
             final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmRequest.getPwmApplication(), chaiUser )
                     .setExpandPwmMacros( true )
-                    .setMacroMachine( macroMachine )
+                    .setMacroMachine( macroRequest )
                     .createActionExecutor();
 
             actionExecutor.executeAction( action, pwmRequest.getLabel() );
@@ -783,9 +783,9 @@ public class HelpdeskServlet extends ControlledPwmServlet
             pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
             return ProcessStatus.Halt;
         }
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), userInfo, null );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), userInfo, null );
         final String configuredTokenString = config.readAppProperty( AppProperty.HELPDESK_TOKEN_VALUE );
-        final String tokenKey = macroMachine.expandMacros( configuredTokenString );
+        final String tokenKey = macroRequest.expandMacros( configuredTokenString );
         final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_HELPDESK_TOKEN, pwmRequest.getLocale() );
 
         LOGGER.debug( pwmRequest, () -> "generated token code for " + userIdentity.toDelimitedKey() );
@@ -798,7 +798,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
                     TokenService.TokenSendInfo.builder()
                             .pwmApplication( pwmRequest.getPwmApplication() )
                             .userInfo( userInfo )
-                            .macroMachine( macroMachine )
+                            .macroRequest( macroRequest )
                             .configuredEmailSetting( emailItemBean )
                             .tokenDestinationItem( tokenDestinationItem )
                             .smsMessage( smsMessage )

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServletUtil.java

@@ -48,7 +48,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.util.Collection;
 import java.util.List;
@@ -308,7 +308,7 @@ public class HelpdeskServletUtil
                 chaiUser.getChaiProvider()
         );
 
-        final MacroMachine macroMachine = MacroMachine.forUser(
+        final MacroRequest macroRequest = MacroRequest.forUser(
                 pwmApplication,
                 pwmRequest.getLabel(),
                 userInfo,
@@ -318,7 +318,7 @@ public class HelpdeskServletUtil
         pwmApplication.getEmailQueue().submitEmail(
                 configuredEmailSetting,
                 userInfo,
-                macroMachine
+                macroRequest
         );
     }
 

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

@@ -62,7 +62,7 @@ import password.pwm.util.java.Percent;
 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.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestCheckPasswordServer;
@@ -282,14 +282,14 @@ public class NewUserServlet extends ControlledPwmServlet
         {
             if ( !newUserBean.isAgreementPassed() )
             {
-                final MacroMachine macroMachine = NewUserUtils.createMacroMachineForNewUser(
+                final MacroRequest macroRequest = NewUserUtils.createMacroMachineForNewUser(
                         pwmApplication,
                         newUserProfile,
                         pwmRequest.getLabel(),
                         newUserBean.getNewUserForm(),
                         null
                 );
-                final String expandedText = macroMachine.expandMacros( newUserAgreementText );
+                final String expandedText = macroRequest.expandMacros( newUserAgreementText );
                 pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
                 pwmRequest.forwardToJsp( JspUrl.NEW_USER_AGREEMENT );
                 return;
@@ -781,8 +781,8 @@ public class NewUserServlet extends ControlledPwmServlet
         final String configuredRedirectUrl = newUserProfile.readSettingAsString( PwmSetting.NEWUSER_REDIRECT_URL );
         if ( !StringUtil.isEmpty( configuredRedirectUrl ) && StringUtil.isEmpty( pwmRequest.getPwmSession().getSessionStateBean().getForwardURL() ) )
         {
-            final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
-            final String macroedUrl = macroMachine.expandMacros( configuredRedirectUrl );
+            final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
+            final String macroedUrl = macroRequest.expandMacros( configuredRedirectUrl );
             pwmRequest.sendRedirect( macroedUrl );
             return ProcessStatus.Halt;
         }

+ 10 - 9
server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java

@@ -72,7 +72,8 @@ 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.macro.MacroRequest;
+import password.pwm.util.macro.MacroReplacer;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.password.RandomPasswordGenerator;
@@ -370,12 +371,12 @@ class NewUserUtils
             throws PwmUnrecoverableException, ChaiUnavailableException
     {
         final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile( pwmRequest );
-        final MacroMachine macroMachine = createMacroMachineForNewUser( pwmRequest.getPwmApplication(), newUserProfile, pwmRequest.getLabel(), formValues, null );
+        final MacroRequest macroRequest = createMacroMachineForNewUser( pwmRequest.getPwmApplication(), newUserProfile, pwmRequest.getLabel(), formValues, null );
         final List<String> configuredNames = newUserProfile.readSettingAsStringArray( PwmSetting.NEWUSER_USERNAME_DEFINITION );
         final List<String> failedValues = new ArrayList<>();
 
         final String configuredContext = newUserProfile.readSettingAsString( PwmSetting.NEWUSER_CONTEXT );
-        final String expandedContext = macroMachine.expandMacros( configuredContext );
+        final String expandedContext = macroRequest.expandMacros( configuredContext );
 
 
         if ( configuredNames == null || configuredNames.isEmpty() || configuredNames.iterator().next().isEmpty() )
@@ -408,7 +409,7 @@ class NewUserUtils
             {
                 {
                     final String configuredName = configuredNames.get( attemptCount );
-                    expandedName = macroMachine.expandMacros( configuredName );
+                    expandedName = macroRequest.expandMacros( configuredName );
                 }
 
                 if ( !testIfEntryNameExists( pwmRequest, expandedName ) )
@@ -510,7 +511,7 @@ class NewUserUtils
             .build();
     }
 
-    static MacroMachine createMacroMachineForNewUser(
+    static MacroRequest createMacroMachineForNewUser(
             final PwmApplication pwmApplication,
             final NewUserProfile newUserProfile,
             final SessionLabel sessionLabel,
@@ -524,11 +525,11 @@ class NewUserUtils
 
         final UserInfoBean stubUserBean = createUserInfoBeanForNewUser( pwmApplication, newUserProfile, newUserForm );
 
-        final MacroMachine.StringReplacer stringReplacer = tokenDestinationItem == null
+        final MacroReplacer macroReplacer = tokenDestinationItem == null
                 ? null
                 : TokenUtil.makeTokenDestStringReplacer( tokenDestinationItem );
 
-        return MacroMachine.forUser( pwmApplication, sessionLabel, stubUserBean, stubLoginBean, stringReplacer );
+        return MacroRequest.forUser( pwmApplication, sessionLabel, stubUserBean, stubLoginBean, macroReplacer );
     }
 
     static Map<String, String> figureDisplayableProfiles( final PwmRequest pwmRequest )
@@ -759,7 +760,7 @@ class NewUserUtils
                     }
 
                     final Map<String, String> tokenPayloadMap = NewUserFormUtils.toTokenPayload( pwmRequest, newUserBean );
-                    final MacroMachine macroMachine = createMacroMachineForNewUser(
+                    final MacroRequest macroRequest = createMacroMachineForNewUser(
                             pwmRequest.getPwmApplication(),
                             newUserProfile,
                             pwmRequest.getLabel(),
@@ -778,7 +779,7 @@ class NewUserUtils
                                     .tokenType( TokenType.NEWUSER )
                                     .smsToSend( PwmSetting.SMS_NEWUSER_TOKEN_TEXT )
                                     .inputTokenData( tokenPayloadMap )
-                                    .macroMachine( macroMachine )
+                                    .macroRequest( macroRequest )
                                     .tokenLifetime( tokenLifetime )
                                     .build()
                     );

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/oauth/OAuthMachine.java

@@ -46,7 +46,7 @@ import password.pwm.util.java.JavaHelper;
 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.util.macro.MacroRequest;
 
 import java.io.IOException;
 import java.net.URI;
@@ -461,8 +461,8 @@ public class OAuthMachine
             return null;
         }
 
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
-        final String username = macroMachine.expandMacros( macroText );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest, userIdentity );
+        final String username = macroRequest.expandMacros( macroText );
         LOGGER.debug( sessionLabel, () -> "calculated username value for user as: " + username );
 
         final String grantUrl = settings.getLoginURL();

+ 9 - 9
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java

@@ -68,7 +68,7 @@ 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.macro.MacroRequest;
 
 import java.io.IOException;
 import java.io.Serializable;
@@ -308,12 +308,12 @@ class PeopleSearchDataReader
             return Collections.emptyList();
         }
         final List<LinkReferenceBean> returnList = new ArrayList<>();
-        final MacroMachine macroMachine = getMacroMachine( actorIdentity );
+        final MacroRequest macroRequest = getMacroMachine( actorIdentity );
         for ( final Map.Entry<String, String> entry : linkMap.entrySet() )
         {
             final String key = entry.getKey();
             final String value = entry.getValue();
-            final String parsedValue = macroMachine.expandMacros( value );
+            final String parsedValue = macroRequest.expandMacros( value );
             final LinkReferenceBean linkReference = new LinkReferenceBean();
             linkReference.setName( key );
             linkReference.setLink( parsedValue );
@@ -487,9 +487,9 @@ class PeopleSearchDataReader
     )
             throws PwmUnrecoverableException
     {
-        final MacroMachine macroMachine = getMacroMachine( userIdentity );
+        final MacroRequest macroRequest = getMacroMachine( userIdentity );
         final String settingValue = this.peopleSearchConfiguration.getDisplayName();
-        return macroMachine.expandMacros( settingValue );
+        return macroRequest.expandMacros( settingValue );
     }
 
     private List<String> figureDisplaynames(
@@ -501,10 +501,10 @@ class PeopleSearchDataReader
         final List<String> displayStringSettings = this.peopleSearchConfiguration.getDisplayNameCardLables();
         if ( displayStringSettings != null )
         {
-            final MacroMachine macroMachine = getMacroMachine( userIdentity );
+            final MacroRequest macroRequest = getMacroMachine( userIdentity );
             for ( final String displayStringSetting : displayStringSettings )
             {
-                final String displayLabel = macroMachine.expandMacros( displayStringSetting );
+                final String displayLabel = macroRequest.expandMacros( displayStringSetting );
                 displayLabels.add( displayLabel );
             }
         }
@@ -585,7 +585,7 @@ class PeopleSearchDataReader
 
 
 
-    MacroMachine getMacroMachine(
+    MacroRequest getMacroMachine(
             final UserIdentity userIdentity
     )
             throws PwmUnrecoverableException
@@ -599,7 +599,7 @@ class PeopleSearchDataReader
                 userIdentity,
                 chaiProvider
         );
-        return MacroMachine.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), userInfo, null );
+        return MacroRequest.forUser( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), userInfo, null );
     }
 
     void checkIfUserIdentityViewable(

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/peoplesearch/PhotoDataReader.java

@@ -49,7 +49,7 @@ import password.pwm.util.java.JavaHelper;
 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.macro.MacroRequest;
 
 import javax.servlet.http.HttpServletResponse;
 import java.io.OutputStream;
@@ -267,8 +267,8 @@ public class PhotoDataReader
 
         if ( !StringUtil.isEmpty( configuredUrl ) )
         {
-            final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest.commonValues(), userIdentity );
-            return Optional.of( macroMachine.expandMacros( configuredUrl ) );
+            final MacroRequest macroRequest = MacroRequest.forUser( pwmRequest.commonValues(), userIdentity );
+            return Optional.of( macroRequest.expandMacros( configuredUrl ) );
 
         }
 

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

@@ -57,7 +57,7 @@ import password.pwm.util.java.JavaHelper;
 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.util.macro.MacroRequest;
 import password.pwm.ws.server.RestResultBean;
 
 import javax.servlet.ServletException;
@@ -349,8 +349,8 @@ public class UpdateProfileServlet extends ControlledPwmServlet
             {
                 if ( !updateProfileBean.isAgreementPassed() )
                 {
-                    final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-                    final String expandedText = macroMachine.expandMacros( updateProfileAgreementText );
+                    final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+                    final String expandedText = macroRequest.expandMacros( updateProfileAgreementText );
                     pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
                     pwmRequest.forwardToJsp( JspUrl.UPDATE_ATTRIBUTES_AGREEMENT );
                     return;

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

@@ -52,7 +52,7 @@ import password.pwm.util.java.JavaHelper;
 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.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 
 import javax.servlet.ServletException;
@@ -148,7 +148,7 @@ public class UpdateProfileUtil
 
     static void sendProfileUpdateEmailNotice(
             final PwmApplication pwmApplication,
-            final MacroMachine macroMachine,
+            final MacroRequest macroRequest,
             final UserInfo userInfo,
             final Locale locale,
             final SessionLabel sessionLabel
@@ -161,7 +161,7 @@ public class UpdateProfileUtil
         pwmApplication.getEmailQueue().submitEmail(
                 configuredEmailSetting,
                 userInfo,
-                macroMachine
+                macroRequest
         );
 
         if ( configuredEmailSetting == null )
@@ -347,7 +347,7 @@ public class UpdateProfileUtil
             final SessionLabel sessionLabel,
             final Locale locale,
             final UserInfo userInfo,
-            final MacroMachine macroMachine,
+            final MacroRequest macroRequest,
             final UpdateProfileProfile updateProfileProfile,
             final Map<String, String> formValues,
             final ChaiUser theUser
@@ -363,7 +363,7 @@ public class UpdateProfileUtil
         // write values.
         LOGGER.info( () -> "updating profile for " + userInfo.getUserIdentity() );
 
-        LdapOperationsHelper.writeFormValuesToLdap( theUser, formMap, macroMachine, false );
+        LdapOperationsHelper.writeFormValuesToLdap( theUser, formMap, macroRequest, false );
 
         postUpdateActionsAndEmail( pwmApplication, sessionLabel, locale, userInfo.getUserIdentity(), updateProfileProfile );
 
@@ -387,7 +387,7 @@ public class UpdateProfileUtil
                 locale,
                 userIdentity,
                 pwmApplication.getProxiedChaiUser( userIdentity ).getChaiProvider() );
-        final MacroMachine reloadedMacroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, reloadedUserInfo, null, null );
+        final MacroRequest reloadedMacroRequest = MacroRequest.forUser( pwmApplication, sessionLabel, reloadedUserInfo, null, null );
 
         {
             // execute configured actions
@@ -398,14 +398,14 @@ public class UpdateProfileUtil
 
                 final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, reloadedUserInfo.getUserIdentity() )
                         .setExpandPwmMacros( true )
-                        .setMacroMachine( reloadedMacroMachine )
+                        .setMacroMachine( reloadedMacroRequest )
                         .createActionExecutor();
 
                 actionExecutor.executeActions( actions, sessionLabel );
             }
         }
 
-        sendProfileUpdateEmailNotice( pwmApplication, reloadedMacroMachine, reloadedUserInfo, locale, sessionLabel );
+        sendProfileUpdateEmailNotice( pwmApplication, reloadedMacroRequest, reloadedUserInfo, locale, sessionLabel );
     }
 
     static TokenDestinationItem tokenDestinationItemForCurrentValidation(

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

@@ -28,7 +28,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.i18n.Display;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -133,8 +133,8 @@ public class DisplayTag extends PwmAbstractTag
 
             if ( pwmRequest != null )
             {
-                final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-                displayMessage = macroMachine.expandMacros( displayMessage );
+                final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+                displayMessage = macroRequest.expandMacros( displayMessage );
             }
 
             pageContext.getOut().write( displayMessage );

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

@@ -30,7 +30,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -88,8 +88,8 @@ public class ErrorMessageTag extends PwmAbstractTag
 
                 outputMsg = outputMsg.replace( "\n", "<br/>" );
 
-                final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-                outputMsg = macroMachine.expandMacros( outputMsg );
+                final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+                outputMsg = macroRequest.expandMacros( outputMsg );
 
                 pageContext.getOut().write( outputMsg );
             }

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

@@ -36,7 +36,7 @@ import password.pwm.i18n.Message;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.password.PasswordRuleReaderHelper;
 
 import javax.servlet.http.HttpServletRequest;
@@ -65,11 +65,11 @@ public class PasswordRequirementsTag extends TagSupport
             final PwmPasswordPolicy passwordPolicy,
             final Configuration config,
             final Locale locale,
-            final MacroMachine macroMachine
+            final MacroRequest macroRequest
     )
     {
         final List<String> ruleTexts = new ArrayList<>(  );
-        final PolicyValues policyValues = new PolicyValues( passwordPolicy, passwordPolicy.getRuleHelper(), locale, config, macroMachine );
+        final PolicyValues policyValues = new PolicyValues( passwordPolicy, passwordPolicy.getRuleHelper(), locale, config, macroRequest );
         for ( final RuleTextGenerator ruleTextGenerator : RULE_TEXT_GENERATORS )
         {
             ruleTexts.addAll( ruleTextGenerator.generate( policyValues ) );
@@ -114,7 +114,7 @@ public class PasswordRequirementsTag extends TagSupport
         private PasswordRuleReaderHelper ruleHelper;
         private Locale locale;
         private Configuration config;
-        private MacroMachine macroMachine;
+        private MacroRequest macroRequest;
     }
 
     private static class CaseSensitiveRuleTextGenerator implements RuleTextGenerator
@@ -387,7 +387,7 @@ public class PasswordRequirementsTag extends TagSupport
                 {
                     fieldValue.append( " " );
 
-                    final String expandedValue = policyValues.getMacroMachine().expandMacros( loopValue );
+                    final String expandedValue = policyValues.getMacroRequest().expandMacros( loopValue );
                     fieldValue.append( StringUtil.escapeHtml( expandedValue ) );
                 }
                 return Collections.singletonList( getLocalString( Message.Requirement_DisAllowedValues, fieldValue.toString(), policyValues ) );
@@ -594,11 +594,11 @@ public class PasswordRequirementsTag extends TagSupport
             }
             else
             {
-                final MacroMachine macroMachine = pwmSession.getSessionManager().getMacroMachine( );
+                final MacroRequest macroRequest = pwmSession.getSessionManager().getMacroMachine( );
 
                 final String pre = prepend != null && prepend.length() > 0 ? prepend : "";
                 final String sep = separator != null && separator.length() > 0 ? separator : "<br/>";
-                final List<String> requirementsList = getPasswordRequirementsStrings( passwordPolicy, config, locale, macroMachine );
+                final List<String> requirementsList = getPasswordRequirementsStrings( passwordPolicy, config, locale, macroRequest );
 
                 final StringBuilder requirementsText = new StringBuilder();
                 for ( final String requirementStatement : requirementsList )

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

@@ -23,7 +23,7 @@ package password.pwm.http.tag;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -53,8 +53,8 @@ public class PwmMacroTag extends TagSupport
         try
         {
             final PwmRequest pwmRequest = PwmRequest.forRequest( ( HttpServletRequest ) pageContext.getRequest(), ( HttpServletResponse ) pageContext.getResponse() );
-            final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-            final String outputValue = macroMachine.expandMacros( value );
+            final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+            final String outputValue = macroRequest.expandMacros( value );
             pageContext.getOut().write( outputValue );
         }
         catch ( final PwmUnrecoverableException e )

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

@@ -23,7 +23,7 @@ package password.pwm.http.tag;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.i18n.Message;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -55,8 +55,8 @@ public class SuccessMessageTag extends PwmAbstractTag
             {
                 if ( pwmRequest.isAuthenticated() )
                 {
-                    final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
-                    outputMsg = macroMachine.expandMacros( successMsg );
+                    final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+                    outputMsg = macroRequest.expandMacros( successMsg );
                 }
                 else
                 {

+ 5 - 5
server/src/main/java/password/pwm/http/tag/value/PwmValue.java

@@ -35,7 +35,7 @@ import password.pwm.i18n.Admin;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import javax.servlet.jsp.JspPage;
 import javax.servlet.jsp.PageContext;
@@ -110,8 +110,8 @@ public enum PwmValue
             {
                 try
                 {
-                    final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
-                    outputURL = macroMachine.expandMacros( outputURL );
+                    final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
+                    outputURL = macroRequest.expandMacros( outputURL );
                 }
                 catch ( final PwmUnrecoverableException e )
                 {
@@ -153,8 +153,8 @@ public enum PwmValue
             {
                 try
                 {
-                    final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
-                    final String expandedScript = macroMachine.expandMacros( customScript );
+                    final MacroRequest macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine();
+                    final String expandedScript = macroRequest.expandMacros( customScript );
                     return expandedScript;
                 }
                 catch ( final Exception e )

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

@@ -61,7 +61,7 @@ import password.pwm.util.i18n.LocaleHelper;
 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.macro.MacroRequest;
 import password.pwm.util.secure.PwmTrustManager;
 
 import javax.net.ssl.X509TrustManager;
@@ -270,7 +270,7 @@ public class LdapOperationsHelper
      *
      * @param theUser User to write to.
      * @param valueMap A map with String keys and String values.
-     * @param macroMachine used to resolve macros before values are written.
+     * @param macroRequest used to resolve macros before values are written.
      * @param expandMacros a boolean to indicate if value macros should be expanded.
      * @throws ChaiUnavailableException if the directory is unavailable
      * @throws PwmUnrecoverableException if their is an unexpected ldap problem
@@ -278,7 +278,7 @@ public class LdapOperationsHelper
     public static void writeFormValuesToLdap(
             final ChaiUser theUser,
             final Map<FormConfiguration, String> valueMap,
-            final MacroMachine macroMachine,
+            final MacroRequest macroRequest,
             final boolean expandMacros
     )
             throws PwmUnrecoverableException, ChaiUnavailableException
@@ -370,7 +370,7 @@ public class LdapOperationsHelper
 
                     if ( expandMacros )
                     {
-                        attrValue = macroMachine.expandMacros( attrValue );
+                        attrValue = macroRequest.expandMacros( attrValue );
                     }
 
                     final String currentValue;
@@ -593,9 +593,9 @@ public class LdapOperationsHelper
         )
                 throws PwmUnrecoverableException
         {
-            final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, sessionLabel );
+            final MacroRequest macroRequest = MacroRequest.forNonUserSpecific( pwmApplication, sessionLabel );
             final String guidPattern = pwmApplication.getConfig().readAppProperty( AppProperty.LDAP_GUID_PATTERN );
-            return macroMachine.expandMacros( guidPattern );
+            return macroRequest.expandMacros( guidPattern );
         }
     }
 

+ 3 - 3
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -61,7 +61,7 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 
 import java.time.Instant;
@@ -361,8 +361,8 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
                 ? "none"
                 : authenticationResult.getUserProvider().getChaiConfiguration().getSetting( ChaiSetting.BIND_DN ) ) );
 
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, sessionLabel, userIdentity );
-        final AuditRecord auditRecord = new AuditRecordFactory( pwmApplication, macroMachine ).createUserAuditRecord(
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, sessionLabel, userIdentity );
+        final AuditRecord auditRecord = new AuditRecordFactory( pwmApplication, macroRequest ).createUserAuditRecord(
                 AuditEvent.AUTHENTICATE,
                 this.userIdentity,
                 makeAuditLogMessage( authenticationResult.getAuthenticationType() ),

+ 7 - 7
server/src/main/java/password/pwm/svc/email/EmailServerUtil.java

@@ -40,7 +40,7 @@ import password.pwm.util.java.JavaHelper;
 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.macro.MacroRequest;
 import password.pwm.util.secure.PwmTrustManager;
 import password.pwm.util.secure.CertificateReadingTrustManager;
 import password.pwm.util.secure.X509Utils;
@@ -244,15 +244,15 @@ public class EmailServerUtil
         return new InternetAddress( input );
     }
 
-    static EmailItemBean applyMacrosToEmail( final EmailItemBean emailItem, final MacroMachine macroMachine )
+    static EmailItemBean applyMacrosToEmail( final EmailItemBean emailItem, final MacroRequest macroRequest )
     {
         final EmailItemBean expandedEmailItem;
         expandedEmailItem = new EmailItemBean(
-                macroMachine.expandMacros( emailItem.getTo() ),
-                macroMachine.expandMacros( emailItem.getFrom() ),
-                macroMachine.expandMacros( emailItem.getSubject() ),
-                macroMachine.expandMacros( emailItem.getBodyPlain() ),
-                macroMachine.expandMacros( emailItem.getBodyHtml() )
+                macroRequest.expandMacros( emailItem.getTo() ),
+                macroRequest.expandMacros( emailItem.getFrom() ),
+                macroRequest.expandMacros( emailItem.getSubject() ),
+                macroRequest.expandMacros( emailItem.getBodyPlain() ),
+                macroRequest.expandMacros( emailItem.getBodyHtml() )
         );
         return expandedEmailItem;
     }

+ 11 - 11
server/src/main/java/password/pwm/svc/email/EmailService.java

@@ -46,7 +46,7 @@ import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBStoredQueue;
 import password.pwm.util.localdb.WorkQueueProcessor;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import javax.mail.Message;
 import javax.mail.MessagingException;
@@ -327,27 +327,27 @@ public class EmailService implements PwmService
     public void submitEmail(
             final EmailItemBean emailItem,
             final UserInfo userInfo,
-            final MacroMachine macroMachine
+            final MacroRequest macroRequest
     )
             throws PwmUnrecoverableException
     {
-        submitEmailImpl( emailItem, userInfo, macroMachine, false );
+        submitEmailImpl( emailItem, userInfo, macroRequest, false );
     }
 
     public void submitEmailImmediate(
             final EmailItemBean emailItem,
             final UserInfo userInfo,
-            final MacroMachine macroMachine
+            final MacroRequest macroRequest
     )
             throws PwmUnrecoverableException
     {
-        submitEmailImpl( emailItem, userInfo, macroMachine, true );
+        submitEmailImpl( emailItem, userInfo, macroRequest, true );
     }
 
     private void submitEmailImpl(
             final EmailItemBean emailItem,
             final UserInfo userInfo,
-            final MacroMachine macroMachine,
+            final MacroRequest macroRequest,
             final boolean immediate
     )
             throws PwmUnrecoverableException
@@ -371,9 +371,9 @@ public class EmailService implements PwmService
                     workingItemBean = EmailServerUtil.newEmailToAddress( workingItemBean, toAddress );
                 }
 
-                if ( macroMachine != null )
+                if ( macroRequest != null )
                 {
-                    workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
+                    workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroRequest );
                 }
 
                 if ( StringUtil.isEmpty( workingItemBean.getTo() ) )
@@ -425,16 +425,16 @@ public class EmailService implements PwmService
             final EmailServer emailServer,
             final Configuration configuration,
             final EmailItemBean emailItem,
-            final MacroMachine macroMachine
+            final MacroRequest macroRequest
     )
             throws PwmOperationalException, PwmUnrecoverableException, MessagingException
 
     {
         validateEmailItem( emailItem );
         EmailItemBean workingItemBean = emailItem;
-        if ( macroMachine != null )
+        if ( macroRequest != null )
         {
-            workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
+            workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroRequest );
         }
         final Transport transport = EmailServerUtil.makeSmtpTransport( emailServer );
         final List<Message> messages = EmailServerUtil.convertEmailItemToMessages(

+ 9 - 9
server/src/main/java/password/pwm/svc/event/AuditRecordFactory.java

@@ -33,7 +33,7 @@ import password.pwm.ldap.UserInfoFactory;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.time.Instant;
 import java.util.Map;
@@ -43,30 +43,30 @@ public class AuditRecordFactory
     private static final PwmLogger LOGGER = PwmLogger.forClass( AuditRecordFactory.class );
 
     private final PwmApplication pwmApplication;
-    private final MacroMachine macroMachine;
+    private final MacroRequest macroRequest;
 
     public AuditRecordFactory( final PwmApplication pwmApplication ) throws PwmUnrecoverableException
     {
         this.pwmApplication = pwmApplication;
-        this.macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, null );
+        this.macroRequest = MacroRequest.forNonUserSpecific( pwmApplication, null );
     }
 
-    public AuditRecordFactory( final PwmApplication pwmApplication, final MacroMachine macroMachine )
+    public AuditRecordFactory( final PwmApplication pwmApplication, final MacroRequest macroRequest )
     {
         this.pwmApplication = pwmApplication;
-        this.macroMachine = macroMachine;
+        this.macroRequest = macroRequest;
     }
 
     public AuditRecordFactory( final PwmApplication pwmApplication, final PwmRequest pwmRequest ) throws PwmUnrecoverableException
     {
         this.pwmApplication = pwmApplication;
-        this.macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+        this.macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
     }
 
     public AuditRecordFactory( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
     {
         this.pwmApplication = pwmRequest.getPwmApplication();
-        this.macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
+        this.macroRequest = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( );
     }
 
     public HelpdeskAuditRecord createHelpdeskAuditRecord(
@@ -204,9 +204,9 @@ public class AuditRecordFactory
 
         String outputString = LocaleHelper.getLocalizedMessage( PwmConstants.DEFAULT_LOCALE, pwmDisplayBundle, pwmApplication.getConfig() );
 
-        if ( macroMachine != null )
+        if ( macroRequest != null )
         {
-            outputString = macroMachine.expandMacros( outputString );
+            outputString = macroRequest.expandMacros( outputString );
         }
 
         final Map<String, String> recordFields = JsonUtil.deserializeStringMap( JsonUtil.serialize( auditRecord ) );

+ 4 - 4
server/src/main/java/password/pwm/svc/event/AuditService.java

@@ -52,7 +52,7 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -298,9 +298,9 @@ public class AuditService implements PwmService
     )
             throws PwmUnrecoverableException
     {
-        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.AUDITING_SESSION_LABEL );
+        final MacroRequest macroRequest = MacroRequest.forNonUserSpecific( pwmApplication, SessionLabel.AUDITING_SESSION_LABEL );
 
-        String subject = macroMachine.expandMacros( pwmApplication.getConfig().readAppProperty( AppProperty.AUDIT_EVENTS_EMAILSUBJECT ) );
+        String subject = macroRequest.expandMacros( pwmApplication.getConfig().readAppProperty( AppProperty.AUDIT_EVENTS_EMAILSUBJECT ) );
         subject = subject.replace( "%EVENT%", record.getEventCode().getLocalizedString( pwmApplication.getConfig(), PwmConstants.DEFAULT_LOCALE ) );
 
         final String body;
@@ -316,7 +316,7 @@ public class AuditService implements PwmService
                 .subject( subject )
                 .bodyPlain( body )
                 .build();
-        pwmApplication.getEmailQueue().submitEmail( emailItem, null, macroMachine );
+        pwmApplication.getEmailQueue().submitEmail( emailItem, null, macroRequest );
     }
 
     public Instant eldestVaultRecord( )

+ 3 - 3
server/src/main/java/password/pwm/svc/event/CEFAuditFormatter.java

@@ -33,7 +33,7 @@ import password.pwm.util.java.JavaHelper;
 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.util.macro.MacroRequest;
 
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -173,9 +173,9 @@ public class CEFAuditFormatter implements AuditFormatter
     private void appendCefHeader( final PwmApplication pwmApplication, final StringBuilder cefOutput, final String value )
             throws PwmUnrecoverableException
     {
-        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL );
+        final MacroRequest macroRequest = MacroRequest.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL );
         cefOutput.append( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR );
-        cefOutput.append( macroMachine.expandMacros( value ) );
+        cefOutput.append( macroRequest.expandMacros( value ) );
     }
 
     private void appendCefValue( final String name, final String value, final StringBuilder cefOutput, final Settings settings )

+ 3 - 3
server/src/main/java/password/pwm/svc/intruder/IntruderManager.java

@@ -62,7 +62,7 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBDataStore;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.secure.PwmRandom;
 
 import java.net.InetAddress;
@@ -697,14 +697,14 @@ public class IntruderManager implements PwmService
                     userIdentity, locale
             );
 
-            final MacroMachine macroMachine = MacroMachine.forUser(
+            final MacroRequest macroRequest = MacroRequest.forUser(
                     pwmApplication,
                     sessionLabel,
                     userInfo,
                     null
             );
 
-            pwmApplication.getEmailQueue().submitEmail( configuredEmailSetting, userInfo, macroMachine );
+            pwmApplication.getEmailQueue().submitEmail( configuredEmailSetting, userInfo, macroRequest );
         }
         catch ( final PwmUnrecoverableException e )
         {

+ 3 - 3
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java

@@ -42,7 +42,7 @@ import password.pwm.util.java.ConditionalTaskExecutor;
 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.macro.MacroRequest;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -332,7 +332,7 @@ public class PwNotifyEngine
                 userIdentity
         );
         final Locale ldapLocale = LocaleHelper.parseLocaleString( userInfoBean.getLanguage() );
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, ldapLocale, SESSION_LABEL, userIdentity );
+        final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, ldapLocale, SESSION_LABEL, userIdentity );
         final EmailItemBean emailItemBean = pwmApplication.getConfig().readSettingAsEmail(
                 PwmSetting.EMAIL_PW_EXPIRATION_NOTICE,
                 ldapLocale
@@ -340,7 +340,7 @@ public class PwNotifyEngine
 
         noticeCount.incrementAndGet();
         StatisticsManager.incrementStat( pwmApplication, Statistic.PWNOTIFY_EMAILS_SENT );
-        pwmApplication.getEmailQueue().submitEmail( emailItemBean, userInfoBean, macroMachine );
+        pwmApplication.getEmailQueue().submitEmail( emailItemBean, userInfoBean, macroRequest );
     }
 
     private void log( final String output )

+ 2 - 2
server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java

@@ -51,7 +51,7 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.secure.PwmRandom;
 
 import java.io.IOException;
@@ -168,7 +168,7 @@ public class TelemetryService implements PwmService
 
         try
         {
-            final String macrodSettings = MacroMachine.forNonUserSpecific( pwmApplication, null ).expandMacros( settings.getSenderSettings() );
+            final String macrodSettings = MacroRequest.forNonUserSpecific( pwmApplication, null ).expandMacros( settings.getSenderSettings() );
             telemetrySender.init( pwmApplication, macrodSettings );
         }
         catch ( final Exception e )

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

@@ -67,7 +67,7 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBDataStore;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.secure.PwmRandom;
 
@@ -704,7 +704,7 @@ public class TokenService implements PwmService
     {
         private PwmApplication pwmApplication;
         private UserInfo userInfo;
-        private MacroMachine macroMachine;
+        private MacroRequest macroRequest;
         private EmailItemBean configuredEmailSetting;
         private TokenDestinationItem tokenDestinationItem;
         private String smsMessage;
@@ -766,7 +766,7 @@ public class TokenService implements PwmService
             pwmApplication.getEmailQueue().submitEmailImmediate(
                     tokenizedEmail,
                     tokenSendInfo.getUserInfo(),
-                    tokenSendInfo.getMacroMachine() );
+                    tokenSendInfo.getMacroRequest() );
 
             LOGGER.debug( () -> "token email added to send queue for " + toAddress );
             return true;
@@ -789,7 +789,7 @@ public class TokenService implements PwmService
             final PwmApplication pwmApplication = tokenSendInfo.getPwmApplication();
             pwmApplication.getIntruderManager().mark( RecordType.TOKEN_DEST, smsNumber, tokenSendInfo.getSessionLabel() );
 
-            pwmApplication.sendSmsUsingQueue( smsNumber, modifiedMessage, tokenSendInfo.getSessionLabel(), tokenSendInfo.getMacroMachine() );
+            pwmApplication.sendSmsUsingQueue( smsNumber, modifiedMessage, tokenSendInfo.getSessionLabel(), tokenSendInfo.getMacroRequest() );
             LOGGER.debug( () -> "token SMS added to send queue for " + smsNumber );
             return true;
         }

+ 10 - 9
server/src/main/java/password/pwm/svc/token/TokenUtil.java

@@ -41,7 +41,8 @@ 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.macro.MacroRequest;
+import password.pwm.util.macro.MacroReplacer;
 import password.pwm.ws.client.rest.RestTokenDataClient;
 
 import java.time.Instant;
@@ -214,15 +215,15 @@ public class TokenUtil
         final Configuration config = commonValues.getConfig();
         final UserInfo userInfo = tokenInitAndSendRequest.getUserInfo();
         final Map<String, String> tokenMapData = new LinkedHashMap<>();
-        final MacroMachine macroMachine;
+        final MacroRequest macroRequest;
         {
-            if ( tokenInitAndSendRequest.getMacroMachine() != null )
+            if ( tokenInitAndSendRequest.getMacroRequest() != null )
             {
-                macroMachine = tokenInitAndSendRequest.getMacroMachine();
+                macroRequest = tokenInitAndSendRequest.getMacroRequest();
             }
             else if ( tokenInitAndSendRequest.getUserInfo() != null )
             {
-                macroMachine = MacroMachine.forUser(
+                macroRequest = MacroRequest.forUser(
                         commonValues.getPwmApplication(),
                         commonValues.getLocale(),
                         commonValues.getSessionLabel(),
@@ -231,7 +232,7 @@ public class TokenUtil
             }
             else
             {
-                macroMachine = null;
+                macroRequest = null;
             }
         }
 
@@ -288,7 +289,7 @@ public class TokenUtil
                 TokenService.TokenSendInfo.builder()
                         .pwmApplication( commonValues.getPwmApplication() )
                         .userInfo( userInfo )
-                        .macroMachine( macroMachine )
+                        .macroRequest( macroRequest )
                         .configuredEmailSetting( emailItemBean )
                         .tokenDestinationItem( tokenInitAndSendRequest.getTokenDestinationItem() )
                         .smsMessage( smsMessage )
@@ -298,7 +299,7 @@ public class TokenUtil
         );
     }
 
-    public static MacroMachine.StringReplacer makeTokenDestStringReplacer( final TokenDestinationItem tokenDestinationItem )
+    public static MacroReplacer makeTokenDestStringReplacer( final TokenDestinationItem tokenDestinationItem )
     {
         return ( matchedMacro, newValue ) ->
         {
@@ -321,7 +322,7 @@ public class TokenUtil
         private TokenType tokenType;
         private PwmSetting smsToSend;
         private Map<String, String> inputTokenData;
-        private MacroMachine macroMachine;
+        private MacroRequest macroRequest;
         private TimeDuration tokenLifetime;
     }
 }

+ 2 - 2
server/src/main/java/password/pwm/util/DailySummaryJob.java

@@ -34,7 +34,7 @@ import password.pwm.svc.report.ReportSummaryData;
 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.macro.MacroRequest;
 
 import java.time.Instant;
 import java.util.Collection;
@@ -102,7 +102,7 @@ public class DailySummaryJob implements Runnable
             makeEmailBody( pwmApplication, dailyStatistics, locale, textBody, htmlBody );
             final EmailItemBean emailItem = new EmailItemBean( toAddress, fromAddress, subject, textBody.toString(), htmlBody.toString() );
             LOGGER.debug( () -> "sending daily summary email to " + toAddress );
-            pwmApplication.getEmailQueue().submitEmail( emailItem, null, MacroMachine.forNonUserSpecific( pwmApplication, null ) );
+            pwmApplication.getEmailQueue().submitEmail( emailItem, null, MacroRequest.forNonUserSpecific( pwmApplication, null ) );
         }
     }
 

+ 3 - 3
server/src/main/java/password/pwm/util/i18n/LocaleHelper.java

@@ -35,7 +35,7 @@ import password.pwm.i18n.PwmLocaleBundle;
 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.macro.MacroRequest;
 
 import java.time.Instant;
 import java.util.ArrayList;
@@ -169,8 +169,8 @@ public class LocaleHelper
             }
         }
 
-        final MacroMachine macroMachine = MacroMachine.forStatic();
-        return macroMachine.expandMacros( returnValue );
+        final MacroRequest macroRequest = MacroRequest.forStatic();
+        return macroRequest.expandMacros( returnValue );
     }
 
     private static ResourceBundle getMessageBundle( final Locale locale, final Class bundleClass )

+ 84 - 3
server/src/main/java/password/pwm/util/macro/AbstractMacro.java

@@ -20,11 +20,19 @@
 
 package password.pwm.util.macro;
 
+import password.pwm.PwmConstants;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.PwmDateFormat;
+
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
+import java.util.TimeZone;
 
-public abstract class AbstractMacro implements MacroImplementation
+public abstract class AbstractMacro implements Macro
 {
     // match param values inside @ and include @ if preceded by /
     static final String PATTERN_OPTIONAL_PARAMETER_MATCH = "(:(?:/@|[^@])*)*";
@@ -67,9 +75,82 @@ public abstract class AbstractMacro implements MacroImplementation
         return returnObj;
     }
 
+    static PwmDateFormat readDateFormatAndTimeZoneParams( final List<String> parameters )
+            throws MacroParseException
+    {
+        final String dateFormatStr;
+        if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
+        {
+            dateFormatStr = parameters.get( 0 );
+        }
+        else
+        {
+            dateFormatStr = PwmConstants.DEFAULT_DATETIME_FORMAT_STR;
+        }
+
+        final TimeZone tz;
+        if ( parameters.size() > 1 && !parameters.get( 1 ).isEmpty() )
+        {
+            final String desiredTz = parameters.get( 1 );
+            final List<String> availableIDs = Arrays.asList( TimeZone.getAvailableIDs() );
+            if ( !availableIDs.contains( desiredTz ) )
+            {
+                throw new MacroParseException( "unknown timezone" );
+            }
+            tz = TimeZone.getTimeZone( desiredTz );
+        }
+        else
+        {
+            tz = PwmConstants.DEFAULT_TIMEZONE;
+        }
+
+        try
+        {
+            return PwmDateFormat.newPwmDateFormat( dateFormatStr, PwmConstants.DEFAULT_LOCALE, tz );
+        }
+        catch ( final IllegalArgumentException e )
+        {
+            throw new MacroParseException( e.getMessage() );
+        }
+    }
+
+    static String processTimeOutputMacro( final Instant timestamp, final String matchValue, final String... ignoreValues )
+            throws MacroParseException
+    {
+        if ( timestamp == null )
+        {
+            return "";
+        }
+
+        final List<String> parameters = splitMacroParameters( matchValue, ignoreValues );
+
+        if ( !parameters.isEmpty() )
+        {
+            final PwmDateFormat pwmDateFormat = readDateFormatAndTimeZoneParams( parameters );
+
+            try
+            {
+                return pwmDateFormat.format( timestamp );
+            }
+            catch ( final IllegalArgumentException e )
+            {
+                throw new MacroParseException( e.getMessage() );
+            }
+        }
+
+        return JavaHelper.toIsoDate( timestamp );
+    }
+
     @Override
-    public MacroDefinitionFlag[] flags( )
+    public Set<MacroDefinitionFlag> flags( )
     {
-        return null;
+        return Collections.emptySet();
     }
+
+    @Override
+    public Sequence getSequence()
+    {
+        return Sequence.normal;
+    }
+
 }

+ 9 - 3
server/src/main/java/password/pwm/util/macro/ExternalRestMacro.java

@@ -63,7 +63,7 @@ class ExternalRestMacro extends AbstractMacro
     @Override
     public String replaceValue(
             final String matchValue,
-            final MacroRequestInfo macroRequestInfo
+            final MacroRequest macroRequestInfo
     )
     {
         final PwmApplication pwmApplication = macroRequestInfo.getPwmApplication();
@@ -76,12 +76,12 @@ class ExternalRestMacro extends AbstractMacro
         {
             if ( userInfoBean != null )
             {
-                final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, userInfoBean.getUserIdentity() );
+                final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, userInfoBean.getUserIdentity() );
                 final PublicUserInfoBean publicUserInfoBean = PublicUserInfoBean.fromUserInfoBean(
                         userInfoBean,
                         pwmApplication.getConfig(),
                         PwmConstants.DEFAULT_LOCALE,
-                        macroMachine
+                        macroRequest
                 );
                 sendData.put( "userInfo", publicUserInfoBean );
             }
@@ -112,4 +112,10 @@ class ExternalRestMacro extends AbstractMacro
             throw new IllegalStateException( errorMsg );
         }
     }
+
+    @Override
+    public Scope getScope()
+    {
+        return Scope.User;
+    }
 }

+ 12 - 25
server/src/main/java/password/pwm/util/macro/MacroImplementation.java → server/src/main/java/password/pwm/util/macro/Macro.java

@@ -20,13 +20,10 @@
 
 package password.pwm.util.macro;
 
-import password.pwm.PwmApplication;
-import password.pwm.bean.LoginInfoBean;
-import password.pwm.ldap.UserInfo;
-
+import java.util.Set;
 import java.util.regex.Pattern;
 
-public interface MacroImplementation
+public interface Macro
 {
     enum Scope
     {
@@ -41,30 +38,20 @@ public interface MacroImplementation
         post,
     }
 
-    Pattern getRegExPattern( );
-
-    String replaceValue( String matchValue, MacroRequestInfo macroRequestInfo )
-            throws MacroParseException;
-
-    interface MacroRequestInfo
+    enum MacroDefinitionFlag
     {
-        PwmApplication getPwmApplication( );
+        SensitiveValue,
+        OnlyDebugLogging,
+    }
 
-        UserInfo getUserInfo( );
+    Pattern getRegExPattern( );
 
-        LoginInfoBean getLoginInfoBean( );
-    }
+    String replaceValue( String matchValue, MacroRequest macroRequestInfo )
+            throws MacroParseException;
 
-    MacroDefinitionFlag[] flags( );
+    Set<MacroDefinitionFlag> flags();
 
-    default Sequence getSequence( )
-    {
-        return Sequence.normal;
-    }
+    Scope getScope();
 
-    enum MacroDefinitionFlag
-    {
-        SensitiveValue,
-        OnlyDebugLogging,
-    }
+    Sequence getSequence( );
 }

+ 110 - 225
server/src/main/java/password/pwm/util/macro/MacroMachine.java

@@ -20,93 +20,76 @@
 
 package password.pwm.util.macro;
 
+import lombok.AllArgsConstructor;
+import lombok.Data;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
-import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.SessionLabel;
-import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.CommonValues;
-import password.pwm.http.PwmRequest;
-import password.pwm.ldap.UserInfo;
-import password.pwm.ldap.UserInfoFactory;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StatisticCounterBundle;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 public class MacroMachine
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( MacroMachine.class );
 
-    private final PwmApplication pwmApplication;
-    private final SessionLabel sessionLabel;
-    private final UserInfo userInfo;
-    private final LoginInfoBean loginInfoBean;
-    private final StringReplacer stringReplacer;
+    private static final Map<Pattern, Macro> BUILTIN_MACROS = makeImplementations();
 
-    private static final Map<MacroImplementation.Scope, Map<Pattern, MacroImplementation>> BUILTIN_MACROS = makeImplementations();
+    private static final StatisticCounterBundle<DebugItem> STATISTIC_COUNTER_BUNDLE = new StatisticCounterBundle<>( DebugItem.class );
 
-    public MacroMachine(
-            final PwmApplication pwmApplication,
-            final SessionLabel sessionLabel,
-            final UserInfo userInfo,
-            final LoginInfoBean loginInfoBean,
-            final StringReplacer stringReplacer
-    )
+    enum DebugItem
     {
-        this.pwmApplication = pwmApplication;
-        this.sessionLabel = sessionLabel;
-        this.userInfo = userInfo;
-        this.loginInfoBean = loginInfoBean;
-        this.stringReplacer = stringReplacer;
+        Matches,
+        Replacements,
+        ExternalInvokes,
     }
 
-    private static Map<MacroImplementation.Scope, Map<Pattern, MacroImplementation>> makeImplementations( )
+    private static Map<Pattern, Macro> makeImplementations( )
     {
-        final Map<Class<? extends MacroImplementation>, MacroImplementation.Scope> implementations = new LinkedHashMap<>();
-        implementations.putAll( StandardMacros.STANDARD_MACROS );
-        implementations.putAll( InternalMacros.INTERNAL_MACROS );
-        final LinkedHashMap<MacroImplementation.Scope, Map<Pattern, MacroImplementation>> map = new LinkedHashMap<>();
-
-        for ( final Map.Entry<Class<? extends MacroImplementation>, MacroImplementation.Scope> entry : implementations.entrySet() )
+        final List<Macro> implementations;
         {
-            final Class macroClass = entry.getKey();
-            final MacroImplementation.Scope scope = entry.getValue();
-            try
-            {
-                final MacroImplementation macroImplementation = ( MacroImplementation ) macroClass.newInstance();
-                final Pattern pattern = macroImplementation.getRegExPattern();
-                if ( !map.containsKey( scope ) )
-                {
-                    map.put( scope, new LinkedHashMap<>() );
-                }
-                map.get( scope ).put( pattern, macroImplementation );
-            }
-            catch ( final Exception e )
-            {
-                LOGGER.error( () -> "unable to load macro class " + macroClass.getName() + ", error: " + e.getMessage() );
-            }
+            final List<Macro> list = new ArrayList<>();
+            list.addAll( SystemMacros.SYSTEM_MACROS );
+            list.addAll( StaticMacros.STATIC_MACROS );
+            list.addAll( UserMacros.USER_MACROS );
+            implementations = Collections.unmodifiableList( list );
         }
 
-        return map;
+        return implementations
+                .stream()
+                .sorted( Comparator.comparing( Macro::getSequence ) )
+                .collect(
+                        Collectors.toMap(
+                                Macro::getRegExPattern,
+                                macroImplementation -> macroImplementation,
+                                ( k, v ) ->
+                                {
+                                    throw new IllegalStateException();
+                                },
+                                LinkedHashMap::new
+                        )
+                );
+
     }
 
-    private Map<Pattern, MacroImplementation> makeExternalImplementations( final PwmApplication pwmApplication )
+    private static Map<Pattern, Macro> makeExternalImplementations( final PwmApplication pwmApplication )
     {
-        final LinkedHashMap<Pattern, MacroImplementation> map = new LinkedHashMap<>();
+        final LinkedHashMap<Pattern, Macro> map = new LinkedHashMap<>();
         final List<String> externalMethods = ( pwmApplication == null )
                 ? Collections.emptyList()
                 : pwmApplication.getConfig().readSettingAsStringArray( PwmSetting.EXTERNAL_MACROS_REST_URLS );
@@ -115,7 +98,7 @@ public class MacroMachine
         for ( final String url : externalMethods )
         {
             iteration++;
-            final MacroImplementation macroImplementation = new ExternalRestMacro( iteration, url );
+            final Macro macroImplementation = new ExternalRestMacro( iteration, url );
             final Pattern pattern = macroImplementation.getRegExPattern();
             map.put( pattern, macroImplementation );
         }
@@ -123,7 +106,8 @@ public class MacroMachine
     }
 
 
-    public String expandMacros(
+    public static String expandMacros(
+            final MacroRequest macroRequest,
             final String input
     )
     {
@@ -137,110 +121,78 @@ public class MacroMachine
             return input;
         }
 
-        final MacroImplementation.MacroRequestInfo macroRequestInfo = new MacroImplementation.MacroRequestInfo()
-        {
-            @Override
-            public PwmApplication getPwmApplication( )
-            {
-                return pwmApplication;
-            }
-
-            @Override
-            public UserInfo getUserInfo( )
-            {
-                return userInfo;
-            }
+        final Set<Macro.Scope> scopes = effectiveScopesForRequest( macroRequest );
 
-            @Override
-            public LoginInfoBean getLoginInfoBean( )
-            {
-                return loginInfoBean;
-            }
-        };
+        final Map<Pattern, Macro> macroImplementations = new LinkedHashMap<>( BUILTIN_MACROS );
 
-        final Set<MacroImplementation.Scope> scopes = effectiveScopes( macroRequestInfo );
-        final Map<Pattern, MacroImplementation> macroImplementations = new LinkedHashMap<>();
         //First the User macros
-        if ( scopes.contains( MacroImplementation.Scope.User ) )
+        if ( scopes.contains( Macro.Scope.User ) )
         {
-            macroImplementations.putAll( makeExternalImplementations( pwmApplication ) );
+            macroImplementations.putAll( makeExternalImplementations( macroRequest.getPwmApplication() ) );
         }
-        //last the buitin macros for Encrypt/Encode to work properly
-        for ( final MacroImplementation.Scope scope : scopes )
-        {
-            macroImplementations.putAll( BUILTIN_MACROS.get( scope ) );
-        }
-
 
-        String workingString = input;
-        final String previousString = workingString;
+        final ReplaceWorkData workData = new ReplaceWorkData( input, input, macroRequest );
 
-        for ( final MacroImplementation.Sequence sequence : MacroImplementation.Sequence.values() )
-        {
-            for ( final Map.Entry<Pattern, MacroImplementation> entry : macroImplementations.entrySet() )
-            {
-                final Pattern pattern = entry.getKey();
-                final MacroImplementation pwmMacro = entry.getValue();
-                if ( pwmMacro.getSequence() == sequence )
-                {
-                    boolean matched = true;
-                    while ( matched )
-                    {
-                        final Matcher matcher = pattern.matcher( workingString );
-                        if ( matcher.find() )
-                        {
-                            workingString = doReplace( workingString, pwmMacro, matcher, macroRequestInfo );
-                            if ( workingString.equals( previousString ) )
-                            {
-                                LOGGER.warn( sessionLabel, () -> "macro replace was called but input string was not modified.  "
-                                        + " macro=" + pwmMacro.getClass().getName() + ", pattern=" + pwmMacro.getRegExPattern().toString() );
-                                break;
-                            }
-                        }
-                        else
-                        {
-                            matched = false;
-                        }
-                    }
-                }
-            }
-        }
+        macroImplementations.entrySet().stream()
+                .filter( entry -> scopes.contains( entry.getValue().getScope() ) )
+                .forEachOrdered( entry -> doRequest( workData,  entry.getKey(), entry.getValue() ) );
 
-        return workingString;
+        return workData.getWorkingString();
     }
 
-    private static Set<MacroImplementation.Scope> effectiveScopes( final MacroImplementation.MacroRequestInfo macroRequestInfo )
+    @Data
+    @AllArgsConstructor
+    private static class ReplaceWorkData
     {
-        final Set<MacroImplementation.Scope> scopes = EnumSet.noneOf( MacroImplementation.Scope.class );
-        scopes.add( MacroImplementation.Scope.Static );
+        private String originalString;
+        private String workingString;
+        private MacroRequest macroRequestInfo;
+    }
 
-        final PwmApplication pwmApplication = macroRequestInfo.getPwmApplication();
-        final PwmApplicationMode mode = pwmApplication != null ? pwmApplication.getApplicationMode() : PwmApplicationMode.ERROR;
-        final boolean appModeOk = mode == PwmApplicationMode.RUNNING || mode == PwmApplicationMode.CONFIGURATION;
-        if ( appModeOk )
+    private static void doRequest(
+            final ReplaceWorkData replaceWorkData,
+            final Pattern pattern,
+            final Macro pwmMacro
+    )
+    {
+        boolean matched;
+        int safetyCounter = 0;
+        do
         {
-            scopes.add( MacroImplementation.Scope.System );
-
-            if ( macroRequestInfo.getUserInfo() != null )
+            safetyCounter++;
+            final Matcher matcher = pattern.matcher( replaceWorkData.getWorkingString() );
+            matched = matcher.find();
+            if ( matched )
             {
-                scopes.add( MacroImplementation.Scope.User );
+                STATISTIC_COUNTER_BUNDLE.increment( DebugItem.Matches );
+                replaceWorkData.setWorkingString( doReplace( replaceWorkData.getWorkingString(), pwmMacro, matcher, replaceWorkData.getMacroRequestInfo() ) );
+                if ( replaceWorkData.getWorkingString().equals( replaceWorkData.getOriginalString() ) )
+                {
+                    LOGGER.warn(
+                            replaceWorkData.getMacroRequestInfo().getSessionLabel(),
+                            () -> "macro replace was called but input string was not modified.  " + " macro="
+                                    + pwmMacro.getClass().getName() + ", pattern=" + pwmMacro.getRegExPattern().toString() );
+                    break;
+                }
             }
         }
-        return Collections.unmodifiableSet( scopes );
+        while ( matched && safetyCounter < 1000 );
     }
 
-
-    private String doReplace(
+    private static String doReplace(
             final String input,
-            final MacroImplementation macroImplementation,
+            final Macro macroImplementation,
             final Matcher matcher,
-            final MacroImplementation.MacroRequestInfo macroRequestInfo
+            final MacroRequest macroRequestInfo
     )
     {
+        final SessionLabel sessionLabel = macroRequestInfo.getSessionLabel();
+        final PwmApplication pwmApplication = macroRequestInfo.getPwmApplication();
         final Instant startTime = Instant.now();
         final String matchedStr = matcher.group();
         final int startPos = matcher.start();
         final int endPos = matcher.end();
+
         String replaceStr = "";
         try
         {
@@ -268,11 +220,13 @@ public class MacroMachine
             return input;
         }
 
-        if ( stringReplacer != null )
+        final MacroReplacer macroReplacer = macroRequestInfo.getMacroReplacer();
+        if ( macroReplacer != null )
         {
             try
             {
-                replaceStr = stringReplacer.replace( matchedStr, replaceStr );
+                replaceStr = macroReplacer.replace( matchedStr, replaceStr );
+                STATISTIC_COUNTER_BUNDLE.increment( DebugItem.Replacements );
             }
             catch ( final Exception e )
             {
@@ -282,109 +236,40 @@ public class MacroMachine
 
         if ( replaceStr != null && replaceStr.length() > 0 )
         {
-            final boolean sensitive = JavaHelper.enumArrayContainsValue( macroImplementation.flags(), MacroImplementation.MacroDefinitionFlag.SensitiveValue );
-            final boolean debugOnlyLogging = JavaHelper.enumArrayContainsValue( macroImplementation.flags(), MacroImplementation.MacroDefinitionFlag.OnlyDebugLogging );
+            final boolean sensitive = macroImplementation.flags().contains( Macro.MacroDefinitionFlag.SensitiveValue );
+            final boolean debugOnlyLogging = macroImplementation.flags().contains( Macro.MacroDefinitionFlag.OnlyDebugLogging );
             if ( !debugOnlyLogging || ( pwmApplication != null && pwmApplication.getConfig().isDevDebugMode() ) )
             {
                 final String finalReplaceStr = replaceStr;
                 LOGGER.trace( sessionLabel, () -> "replaced macro " + matchedStr + " with value: "
-                        + ( sensitive ? PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT : finalReplaceStr )
-                        + " (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
+                                + ( sensitive ? PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT : finalReplaceStr ),
+                        () -> TimeDuration.fromCurrent( startTime ) );
             }
         }
         return new StringBuilder( input ).replace( startPos, endPos, replaceStr ).toString();
     }
 
-    public static MacroMachine forStatic( )
-    {
-        return new MacroMachine( null, null, null, null, null );
-    }
-
-    public interface StringReplacer
-    {
-        String replace( String matchedMacro, String newValue );
-    }
-
-    public static MacroMachine forUser(
-            final CommonValues commonValues,
-            final UserIdentity userIdentity
-    )
-            throws PwmUnrecoverableException
-    {
-        return forUser( commonValues.getPwmApplication(), commonValues.getLocale(), commonValues.getSessionLabel(), userIdentity );
-    }
-
-    public static MacroMachine forUser(
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity
-    )
-            throws PwmUnrecoverableException
-    {
-        return forUser( pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getLabel(), userIdentity );
-    }
-
-    public static MacroMachine forUser(
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity,
-            final StringReplacer stringReplacer
-    )
-            throws PwmUnrecoverableException
+    private static Set<Macro.Scope> effectiveScopesForRequest( final MacroRequest macroRequestInfo )
     {
-        return forUser( pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getLabel(), userIdentity, stringReplacer );
-    }
+        final Set<Macro.Scope> scopes = EnumSet.noneOf( Macro.Scope.class );
+        scopes.add( Macro.Scope.Static );
 
-    public static MacroMachine forUser(
-            final PwmApplication pwmApplication,
-            final SessionLabel sessionLabel,
-            final UserInfo userInfo,
-            final LoginInfoBean loginInfoBean
-    )
-    {
-        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 );
-    }
+        final PwmApplication pwmApplication = macroRequestInfo.getPwmApplication();
+        final PwmApplicationMode mode = pwmApplication != null ? pwmApplication.getApplicationMode() : PwmApplicationMode.ERROR;
 
-    public static MacroMachine forUser(
-            final PwmApplication pwmApplication,
-            final Locale userLocale,
-            final SessionLabel sessionLabel,
-            final UserIdentity userIdentity
-    )
-            throws PwmUnrecoverableException
-    {
-        final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
-        return new MacroMachine( pwmApplication, sessionLabel, userInfoBean, null, null );
-    }
+        if (
+                mode == PwmApplicationMode.RUNNING
+                        || mode == PwmApplicationMode.CONFIGURATION
+        )
+        {
+            scopes.add( Macro.Scope.System );
+        }
 
-    public static MacroMachine forUser(
-            final PwmApplication pwmApplication,
-            final Locale userLocale,
-            final SessionLabel sessionLabel,
-            final UserIdentity userIdentity,
-            final StringReplacer stringReplacer
-    )
-            throws PwmUnrecoverableException
-    {
-        final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
-        return new MacroMachine( pwmApplication, sessionLabel, userInfoBean, null, stringReplacer );
-    }
+        if ( macroRequestInfo.getUserInfo() != null )
+        {
+            scopes.add( Macro.Scope.User );
+        }
 
-    public static MacroMachine forNonUserSpecific(
-            final PwmApplication pwmApplication,
-            final SessionLabel sessionLabel
-    )
-            throws PwmUnrecoverableException
-    {
-        return new MacroMachine( pwmApplication, sessionLabel, null, null, null );
+        return Collections.unmodifiableSet( scopes );
     }
 }

+ 26 - 0
server/src/main/java/password/pwm/util/macro/MacroReplacer.java

@@ -0,0 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 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.util.macro;
+
+public interface MacroReplacer
+{
+    String replace( String matchedMacro, String newValue );
+}

+ 227 - 0
server/src/main/java/password/pwm/util/macro/MacroRequest.java

@@ -0,0 +1,227 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 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.util.macro;
+
+import com.novell.ldapchai.cr.Answer;
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.LoginInfoBean;
+import password.pwm.bean.ResponseInfoBean;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.option.DataStorageMethod;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
+import password.pwm.http.PwmRequest;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoBean;
+import password.pwm.ldap.UserInfoFactory;
+import password.pwm.util.PasswordData;
+import password.pwm.util.operations.otp.OTPUserRecord;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+
+@Value
+@Builder( toBuilder = true )
+public class MacroRequest
+{
+    private final PwmApplication pwmApplication;
+    private final SessionLabel sessionLabel;
+    private final UserInfo userInfo;
+    private final LoginInfoBean loginInfoBean;
+    private final MacroReplacer macroReplacer;
+
+    public MacroRequest(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserInfo userInfo,
+            final LoginInfoBean loginInfoBean,
+            final MacroReplacer macroReplacer
+    )
+    {
+        this.pwmApplication = pwmApplication;
+        this.sessionLabel = sessionLabel;
+        this.userInfo = userInfo;
+        this.loginInfoBean = loginInfoBean;
+        this.macroReplacer = macroReplacer;
+    }
+
+    public static MacroRequest forStatic( )
+    {
+        return new MacroRequest( null, null, null, null, null );
+    }
+
+    public static MacroRequest forUser(
+            final CommonValues commonValues,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        return forUser( commonValues.getPwmApplication(), commonValues.getLocale(), commonValues.getSessionLabel(), userIdentity );
+    }
+
+    public static MacroRequest forUser(
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        return forUser( pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getLabel(), userIdentity );
+    }
+
+    public static MacroRequest forUser(
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity,
+            final MacroReplacer macroReplacer
+    )
+            throws PwmUnrecoverableException
+    {
+        return forUser( pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getLabel(), userIdentity, macroReplacer );
+    }
+
+    public static MacroRequest forUser(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserInfo userInfo,
+            final LoginInfoBean loginInfoBean
+    )
+    {
+        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, null );
+    }
+
+    public static MacroRequest forUser(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserInfo userInfo,
+            final LoginInfoBean loginInfoBean,
+            final MacroReplacer macroReplacer
+    )
+    {
+        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, macroReplacer );
+    }
+
+    public static MacroRequest forUser(
+            final PwmApplication pwmApplication,
+            final Locale userLocale,
+            final SessionLabel sessionLabel,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, null );
+    }
+
+    public static MacroRequest forUser(
+            final PwmApplication pwmApplication,
+            final Locale userLocale,
+            final SessionLabel sessionLabel,
+            final UserIdentity userIdentity,
+            final MacroReplacer macroReplacer
+    )
+            throws PwmUnrecoverableException
+    {
+        final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, macroReplacer );
+    }
+
+    public static MacroRequest forNonUserSpecific(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel
+    )
+    {
+        return new MacroRequest( pwmApplication, sessionLabel, null, null, null );
+    }
+
+    public String expandMacros( final String input )
+    {
+        return MacroMachine.expandMacros( this, input );
+    }
+
+    public static MacroRequest sampleMacroRequest( final PwmApplication pwmApplication )
+            throws PwmUnrecoverableException
+    {
+        final Map<String, String> attributes = new LinkedHashMap<>();
+        attributes.put( "givenName", "First" );
+        attributes.put( "sn", "Last" );
+        attributes.put( "cn", "FLast" );
+        attributes.put( "fullname", "First Last" );
+        attributes.put( "uid", "FLast" );
+        attributes.put( "mail", "FLast@example.com" );
+        attributes.put( "carLicense", "6YJ S32" );
+        attributes.put( "mobile", "800-555-1212" );
+        attributes.put( "objectClass", "inetOrgPerson" );
+        attributes.put( "personalMobile", "800-555-1313" );
+        attributes.put( "title", "Title" );
+        attributes.put( "c", "USA" );
+        attributes.put( "co", "County" );
+        attributes.put( "description", "User Description" );
+        attributes.put( "department", "Department" );
+        attributes.put( "initials", "M" );
+        attributes.put( "postalcode", "12345-6789" );
+        attributes.put( "samaccountname", "FLast" );
+        attributes.put( "userprincipalname", "FLast" );
+
+
+        final UserIdentity userIdentity = new UserIdentity( "cn=test1,ou=test,o=org", "profile1" );
+        final OTPUserRecord otpUserRecord = new OTPUserRecord();
+        otpUserRecord.setTimestamp( Instant.ofEpochSecond( 941259364 ) );
+        final ResponseInfoBean responseInfoBean = new ResponseInfoBean(
+                Collections.emptyMap(),
+                Collections.emptyMap(),
+                PwmConstants.DEFAULT_LOCALE,
+                8 + 3,
+                null,
+                DataStorageMethod.LOCALDB,
+                Answer.FormatType.PBKDF2
+        );
+        responseInfoBean.setTimestamp( Instant.ofEpochSecond( 941246275 ) );
+        final UserInfoBean userInfoBean = UserInfoBean.builder()
+                .userIdentity( userIdentity )
+                .username( "jrivard" )
+                .userEmailAddress( "zippy@example.com" )
+                .attributes( attributes )
+                .passwordExpirationTime( Instant.ofEpochSecond( 949539661 ) )
+                .responseInfoBean( responseInfoBean )
+                .otpUserRecord( otpUserRecord )
+                .cachedAttributeValues( Collections.singletonMap( "givenName", "Jason" ) )
+                .build();
+
+        final LoginInfoBean loginInfoBean = new LoginInfoBean();
+        loginInfoBean.setAuthenticated( true );
+        loginInfoBean.setUserIdentity( userIdentity );
+        loginInfoBean.setUserCurrentPassword( PasswordData.forStringValue( "PaSSw0rd" ) );
+
+        return MacroRequest.builder()
+                .pwmApplication( pwmApplication )
+                .userInfo( userInfoBean )
+                .loginInfoBean( loginInfoBean )
+                .build();
+
+    }
+
+}

+ 0 - 892
server/src/main/java/password/pwm/util/macro/StandardMacros.java

@@ -1,892 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2020 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.util.macro;
-
-import password.pwm.AppProperty;
-import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
-import password.pwm.bean.LoginInfoBean;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.ldap.UserInfo;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.PwmDateFormat;
-import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmRandom;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TimeZone;
-import java.util.regex.Pattern;
-
-public abstract class StandardMacros
-{
-    private static final PwmLogger LOGGER = PwmLogger.forClass( StandardMacros.class );
-
-    static final Map<Class<? extends MacroImplementation>, MacroImplementation.Scope> STANDARD_MACROS;
-
-    static
-    {
-        final Map<Class<? extends MacroImplementation>, MacroImplementation.Scope> defaultMacros = new LinkedHashMap<>();
-
-        // system macros
-        defaultMacros.put( CurrentTimeMacro.class, MacroImplementation.Scope.System );
-        defaultMacros.put( Iso8601DateTimeMacro.class, MacroImplementation.Scope.System );
-        defaultMacros.put( InstanceIDMacro.class, MacroImplementation.Scope.System );
-        defaultMacros.put( DefaultEmailFromAddressMacro.class, MacroImplementation.Scope.System );
-        defaultMacros.put( SiteURLMacro.class, MacroImplementation.Scope.System );
-        defaultMacros.put( SiteHostMacro.class, MacroImplementation.Scope.System );
-        defaultMacros.put( RandomCharMacro.class, MacroImplementation.Scope.System );
-        defaultMacros.put( RandomNumberMacro.class, MacroImplementation.Scope.System );
-        defaultMacros.put( UUIDMacro.class, MacroImplementation.Scope.System );
-
-        // user or system macros
-        defaultMacros.put( UserIDMacro.class, MacroImplementation.Scope.System );
-
-        // user macros
-        defaultMacros.put( LdapMacro.class, MacroImplementation.Scope.User );
-        defaultMacros.put( UserPwExpirationTimeMacro.class, MacroImplementation.Scope.User );
-        defaultMacros.put( UserPwExpirationTimeDefaultMacro.class, MacroImplementation.Scope.User );
-        defaultMacros.put( UserDaysUntilPwExpireMacro.class, MacroImplementation.Scope.User );
-        defaultMacros.put( UserEmailMacro.class, MacroImplementation.Scope.User );
-        defaultMacros.put( UserPasswordMacro.class, MacroImplementation.Scope.User );
-        defaultMacros.put( UserLdapProfileMacro.class, MacroImplementation.Scope.User );
-        defaultMacros.put( OtpSetupTimeMacro.class, MacroImplementation.Scope.User );
-        defaultMacros.put( ResponseSetupTimeMacro.class, MacroImplementation.Scope.User );
-
-        STANDARD_MACROS = Collections.unmodifiableMap( defaultMacros );
-    }
-
-
-    public static class LdapMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@LDAP" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        ) throws MacroParseException
-        {
-            final UserInfo userInfo = macroRequestInfo.getUserInfo();
-
-            if ( userInfo == null )
-            {
-                return "";
-            }
-
-            final List<String> parameters = splitMacroParameters( matchValue, "LDAP" );
-
-            final String ldapAttr;
-            if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
-            {
-                ldapAttr = parameters.get( 0 );
-            }
-            else
-            {
-                throw new MacroParseException( "required attribute name parameter is missing" );
-            }
-
-            final int length;
-            if ( parameters.size() > 1 && !parameters.get( 1 ).isEmpty() )
-            {
-                try
-                {
-                    length = Integer.parseInt( parameters.get( 1 ) );
-                }
-                catch ( final NumberFormatException e )
-                {
-                    throw new MacroParseException( "error parsing length parameter: " + e.getMessage() );
-                }
-
-                final int maxLengthPermitted = Integer.parseInt( macroRequestInfo.getPwmApplication().getConfig().readAppProperty( AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH ) );
-                if ( length > maxLengthPermitted )
-                {
-                    throw new MacroParseException( "maximum permitted length of LDAP attribute (" + maxLengthPermitted + ") exceeded" );
-                }
-                else if ( length <= 0 )
-                {
-                    throw new MacroParseException( "length parameter must be greater than zero" );
-                }
-            }
-            else
-            {
-                length = 0;
-            }
-
-            final String paddingChar;
-            if ( parameters.size() > 2 && !parameters.get( 2 ).isEmpty() )
-            {
-                paddingChar = parameters.get( 2 );
-            }
-            else
-            {
-                paddingChar = "";
-            }
-
-            if ( parameters.size() > 3 )
-            {
-                throw new MacroParseException( "too many parameters" );
-            }
-
-            final String ldapValue;
-            if ( "dn".equalsIgnoreCase( ldapAttr ) )
-            {
-                ldapValue = userInfo.getUserIdentity().getUserDN();
-            }
-            else
-            {
-                try
-                {
-                    ldapValue = userInfo.readStringAttribute( ldapAttr );
-                }
-                catch ( final PwmUnrecoverableException e )
-                {
-                    LOGGER.trace( () -> "could not replace value for '" + matchValue + "', ldap error: " + e.getMessage() );
-                    return "";
-                }
-
-                if ( ldapValue == null || ldapValue.length() < 1 )
-                {
-                    LOGGER.trace( () -> "could not replace value for '" + matchValue + "', user does not have value for '" + ldapAttr + "'" );
-                    return "";
-                }
-            }
-
-            final StringBuilder returnValue = new StringBuilder();
-            returnValue.append( ldapValue == null
-                    ? ""
-                    : ldapValue );
-
-            if ( length > 0 && length < returnValue.length() )
-            {
-                returnValue.delete( length, returnValue.length() );
-            }
-
-            if ( length > 0 && paddingChar.length() > 0 )
-            {
-                while ( returnValue.length() < length )
-                {
-                    returnValue.append( paddingChar );
-                }
-            }
-
-            return returnValue.toString();
-        }
-    }
-
-    public static class InstanceIDMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@InstanceID@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            final PwmApplication pwmApplication = macroRequestInfo.getPwmApplication();
-
-            if ( pwmApplication == null )
-            {
-                LOGGER.error( () -> "could not replace value for '" + matchValue + "', pwmApplication is null" );
-                return "";
-            }
-
-            return pwmApplication.getInstanceID();
-        }
-    }
-
-    public static class CurrentTimeMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@CurrentTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-
-        ) throws MacroParseException
-        {
-            final List<String> parameters = splitMacroParameters( matchValue, "CurrentTime" );
-
-            final String dateFormatStr;
-            if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
-            {
-                dateFormatStr = parameters.get( 0 );
-            }
-            else
-            {
-                dateFormatStr = PwmConstants.DEFAULT_DATETIME_FORMAT_STR;
-            }
-
-            final TimeZone tz;
-            if ( parameters.size() > 1 && !parameters.get( 1 ).isEmpty() )
-            {
-                final String desiredTz = parameters.get( 1 );
-                final List<String> availableIDs = Arrays.asList( TimeZone.getAvailableIDs() );
-                if ( !availableIDs.contains( desiredTz ) )
-                {
-                    throw new MacroParseException( "unknown timezone" );
-                }
-                tz = TimeZone.getTimeZone( desiredTz );
-            }
-            else
-            {
-                tz = PwmConstants.DEFAULT_TIMEZONE;
-            }
-
-            if ( parameters.size() > 2 )
-            {
-                throw new MacroParseException( "too many parameters" );
-            }
-
-            try
-            {
-                final PwmDateFormat pwmDateFormat = PwmDateFormat.newPwmDateFormat( dateFormatStr, PwmConstants.DEFAULT_LOCALE, tz );
-                return pwmDateFormat.format( Instant.now() );
-            }
-            catch ( final IllegalArgumentException e )
-            {
-                throw new MacroParseException( e.getMessage() );
-            }
-        }
-    }
-
-    public static class Iso8601DateTimeMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@Iso8601" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-
-        ) throws MacroParseException
-        {
-            final List<String> parameters = splitMacroParameters( matchValue, "Iso8601" );
-
-            if ( JavaHelper.isEmpty(  parameters ) || parameters.size() != 1 )
-            {
-                throw new MacroParseException( "exactly one parameter is required" );
-            }
-
-            final String param = parameters.get( 0 );
-
-            if ( "DateTime".equalsIgnoreCase( param ) )
-            {
-                return JavaHelper.toIsoDate( Instant.now() );
-            }
-            else if ( "Date".equalsIgnoreCase( param ) )
-            {
-                return Instant.now().atOffset( ZoneOffset.UTC ).format( DateTimeFormatter.ofPattern( "uuuu-MM-dd" ) );
-            }
-            else if ( "Time".equalsIgnoreCase( param ) )
-            {
-                return Instant.now().atOffset( ZoneOffset.UTC ).format( DateTimeFormatter.ofPattern( "HH:mm:ss" ) );
-            }
-
-            throw new MacroParseException( "unknown parameter" );
-        }
-    }
-
-    public static class UserPwExpirationTimeMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@User:PwExpireTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-
-        ) throws MacroParseException
-        {
-            final UserInfo userInfo = macroRequestInfo.getUserInfo();
-
-            if ( userInfo == null )
-            {
-                return "";
-            }
-
-            final Instant pwdExpirationTime;
-            try
-            {
-                pwdExpirationTime = userInfo.getPasswordExpirationTime();
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                LOGGER.error( () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
-                return "";
-            }
-
-            if ( pwdExpirationTime == null )
-            {
-                return "";
-            }
-
-            final String datePattern = matchValue.substring( 19, matchValue.length() - 1 );
-            if ( datePattern.length() > 0 )
-            {
-                try
-                {
-                    final PwmDateFormat dateFormat = PwmDateFormat.newPwmDateFormat( datePattern );
-                    return dateFormat.format( pwdExpirationTime );
-                }
-                catch ( final IllegalArgumentException e )
-                {
-                    throw new MacroParseException( e.getMessage() );
-                }
-            }
-
-            return JavaHelper.toIsoDate( pwdExpirationTime );
-        }
-    }
-
-    public static class UserPwExpirationTimeDefaultMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@User:PwExpireTime@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            final UserInfo userInfo = macroRequestInfo.getUserInfo();
-
-            if ( userInfo == null )
-            {
-                return "";
-            }
-
-            try
-            {
-                final Instant pwdExpirationTime = userInfo.getPasswordExpirationTime();
-                if ( pwdExpirationTime == null )
-                {
-                    return "";
-                }
-
-                return JavaHelper.toIsoDate( pwdExpirationTime );
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                LOGGER.error( () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
-                return "";
-            }
-        }
-    }
-
-    public static class UserDaysUntilPwExpireMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@User:DaysUntilPwExpire@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            final UserInfo userInfo = macroRequestInfo.getUserInfo();
-
-            if ( userInfo == null )
-            {
-                LOGGER.error( () -> "could not replace value for '" + matchValue + "', userInfoBean is null" );
-                return "";
-            }
-
-            try
-            {
-                final Instant pwdExpirationTime = userInfo.getPasswordExpirationTime();
-                final TimeDuration timeUntilExpiration = TimeDuration.fromCurrent( pwdExpirationTime );
-                final long daysUntilExpiration = timeUntilExpiration.as( TimeDuration.Unit.DAYS );
-                return String.valueOf( daysUntilExpiration );
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                LOGGER.error( () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
-                return "";
-            }
-        }
-    }
-
-    public static class UserIDMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@User:ID@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            final UserInfo userInfo = macroRequestInfo.getUserInfo();
-
-            try
-            {
-                if ( userInfo == null || userInfo.getUsername() == null )
-                {
-                    return "";
-                }
-
-                return userInfo.getUsername();
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                LOGGER.error( () -> "error reading username during macro replacement: " + e.getMessage() );
-                return "";
-            }
-        }
-    }
-
-    public static class UserLdapProfileMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@User:LdapProfile@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            final UserInfo userInfo = macroRequestInfo.getUserInfo();
-
-            if ( userInfo != null )
-            {
-                final UserIdentity userIdentity = userInfo.getUserIdentity();
-                if ( userIdentity != null )
-                {
-                    return userIdentity.getLdapProfileID();
-                }
-            }
-
-            return "";
-        }
-    }
-
-    public static class UserEmailMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@User:Email@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            final UserInfo userInfo = macroRequestInfo.getUserInfo();
-
-            try
-            {
-                if ( userInfo == null || userInfo.getUserEmailAddress() == null )
-                {
-                    return "";
-                }
-
-                return userInfo.getUserEmailAddress();
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                LOGGER.error( () -> "error reading user email address during macro replacement: " + e.getMessage() );
-                return "";
-            }
-        }
-    }
-
-    public static class UserPasswordMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@User:Password@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            final LoginInfoBean loginInfoBean = macroRequestInfo.getLoginInfoBean();
-
-            try
-            {
-                if ( loginInfoBean == null || loginInfoBean.getUserCurrentPassword() == null )
-                {
-                    return "";
-                }
-
-                return loginInfoBean.getUserCurrentPassword().getStringValue();
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                LOGGER.error( () -> "error decrypting in memory password during macro replacement: " + e.getMessage() );
-                return "";
-            }
-        }
-
-        @Override
-        public MacroDefinitionFlag[] flags( )
-        {
-            return new MacroDefinitionFlag[]
-                    {
-                            MacroDefinitionFlag.SensitiveValue,
-                    };
-        }
-    }
-
-    public static class SiteURLMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@SiteURL@|@Site:URL@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            return macroRequestInfo.getPwmApplication().getConfig().readSettingAsString( PwmSetting.PWM_SITE_URL );
-        }
-    }
-
-    public static class DefaultEmailFromAddressMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@DefaultEmailFromAddress@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            return macroRequestInfo.getPwmApplication().getConfig().readSettingAsString( PwmSetting.EMAIL_DEFAULT_FROM_ADDRESS );
-        }
-    }
-
-    public static class SiteHostMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@SiteHost@|@Site:Host@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            try
-            {
-                final String siteUrl = macroRequestInfo.getPwmApplication().getConfig().readSettingAsString( PwmSetting.PWM_SITE_URL );
-                final URL url = new URL( siteUrl );
-                return url.getHost();
-            }
-            catch ( final MalformedURLException e )
-            {
-                LOGGER.error( () -> "unable to parse configured/detected site URL: " + e.getMessage() );
-            }
-            return "";
-        }
-    }
-
-    public static class RandomCharMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@RandomChar(:[^@]*)?@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-                throws MacroParseException
-        {
-            if ( matchValue == null || matchValue.length() < 1 )
-            {
-                return "";
-            }
-
-            final List<String> parameters = splitMacroParameters( matchValue, "RandomChar" );
-            int length = 1;
-            if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
-            {
-                final int maxLengthPermitted = Integer.parseInt( macroRequestInfo.getPwmApplication().getConfig().readAppProperty( AppProperty.MACRO_RANDOM_CHAR_MAX_LENGTH ) );
-                try
-                {
-                    length = Integer.parseInt( parameters.get( 0 ) );
-                    if ( length > maxLengthPermitted )
-                    {
-                        throw new MacroParseException( "maximum permitted length of RandomChar (" + maxLengthPermitted + ") exceeded" );
-                    }
-                    else if ( length <= 0 )
-                    {
-                        throw new MacroParseException( "length of RandomChar (" + maxLengthPermitted + ") must be greater than zero" );
-                    }
-                }
-                catch ( final NumberFormatException e )
-                {
-                    throw new MacroParseException( "error parsing length parameter of RandomChar: " + e.getMessage() );
-                }
-            }
-
-            if ( parameters.size() > 1 && !parameters.get( 1 ).isEmpty() )
-            {
-                final String chars = parameters.get( 1 );
-                return PwmRandom.getInstance().alphaNumericString( chars, length );
-            }
-            else
-            {
-                return PwmRandom.getInstance().alphaNumericString( length );
-            }
-        }
-    }
-
-    public static class RandomNumberMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@RandomNumber(:[^@]*)?@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-                throws MacroParseException
-        {
-            if ( matchValue == null || matchValue.length() < 1 )
-            {
-                return "";
-            }
-
-            final List<String> parameters = splitMacroParameters( matchValue, "RandomNumber" );
-            if ( parameters.size() != 2 )
-            {
-                throw new MacroParseException( "incorrect number of parameter of RandomNumber: "
-                        + parameters.size() + ", should be 2" );
-            }
-
-            final int min;
-            final int max;
-            try
-            {
-                min = Integer.parseInt( parameters.get( 0 ) );
-            }
-            catch ( final NumberFormatException e )
-            {
-                throw new MacroParseException( "error parsing minimum value parameter of RandomNumber: " + e.getMessage() );
-            }
-
-            try
-            {
-                max = Integer.parseInt( parameters.get( 1 ) );
-            }
-            catch ( final NumberFormatException e )
-            {
-                throw new MacroParseException( "error parsing maximum value parameter of RandomNumber: " + e.getMessage() );
-            }
-
-            if ( min > max )
-            {
-                throw new MacroParseException( "minimum value is less than maximum value parameter of RandomNumber" );
-            }
-
-            final int range = max - min;
-            return String.valueOf( PwmRandom.getInstance().nextInt( range ) + min );
-        }
-    }
-
-    public static class UUIDMacro extends AbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@UUID@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequestInfo macroRequestInfo
-        )
-        {
-            return PwmRandom.getInstance().randomUUID().toString();
-        }
-    }
-
-    public static class OtpSetupTimeMacro extends InternalMacros.InternalAbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@OtpSetupTime@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo )
-        {
-            try
-            {
-                final UserInfo userInfo = macroRequestInfo.getUserInfo();
-                if ( userInfo != null && userInfo.getOtpUserRecord() != null && userInfo.getOtpUserRecord().getTimestamp() != null )
-                {
-                    return JavaHelper.toIsoDate( userInfo.getOtpUserRecord().getTimestamp() );
-                }
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                LOGGER.error( () -> "error reading otp setup time during macro replacement: " + e.getMessage() );
-            }
-
-            return "";
-        }
-    }
-
-    public static class ResponseSetupTimeMacro extends InternalMacros.InternalAbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@ResponseSetupTime@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo )
-        {
-            try
-            {
-                final UserInfo userInfo = macroRequestInfo.getUserInfo();
-                if ( userInfo != null && userInfo.getResponseInfoBean() != null && userInfo.getResponseInfoBean().getTimestamp() != null )
-                {
-                    return JavaHelper.toIsoDate( userInfo.getResponseInfoBean().getTimestamp() );
-                }
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                LOGGER.error( () -> "error reading response setup time macro replacement: " + e.getMessage() );
-            }
-            return "";
-        }
-    }
-}

+ 59 - 71
server/src/main/java/password/pwm/util/macro/InternalMacros.java → server/src/main/java/password/pwm/util/macro/StaticMacros.java

@@ -20,59 +20,55 @@
 
 package password.pwm.util.macro;
 
-import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
-import password.pwm.PwmEnvironment;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.ContextManager;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.SecureEngine;
 
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
-public abstract class InternalMacros
+public abstract class StaticMacros
 {
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StaticMacros.class );
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( InternalMacros.class );
+    static final List<Macro> STATIC_MACROS = Collections.unmodifiableList( Stream.of(
+            new PwmSettingReference(),
+            new PwmSettingCategoryReference(),
+            new PwmAppName(),
+            new PwmVendorName(),
+            new EncodingMacro(),
+            new CasingMacro(),
+            new HashingMacro()
+    ).collect( Collectors.toList() ) );
 
-    static final Map<Class<? extends MacroImplementation>, MacroImplementation.Scope> INTERNAL_MACROS;
+    private static final Set<Macro.MacroDefinitionFlag> ONLY_DEBUG_LOG_FLAG = Collections.singleton( Macro.MacroDefinitionFlag.OnlyDebugLogging );
 
-    static
+    private abstract static class StaticAbstractMacro extends AbstractMacro
     {
-        final Map<Class<? extends MacroImplementation>, MacroImplementation.Scope> defaultMacros = new HashMap<>();
-        defaultMacros.put( PwmSettingReference.class, MacroImplementation.Scope.Static );
-        defaultMacros.put( PwmSettingCategoryReference.class, MacroImplementation.Scope.Static );
-        defaultMacros.put( PwmAppName.class, MacroImplementation.Scope.Static );
-        defaultMacros.put( PwmVendorName.class, MacroImplementation.Scope.Static );
-        defaultMacros.put( PwmContextPath.class, MacroImplementation.Scope.System );
-        defaultMacros.put( EncodingMacro.class, MacroImplementation.Scope.Static );
-        defaultMacros.put( CasingMacro.class, MacroImplementation.Scope.Static );
-        defaultMacros.put( HashingMacro.class, MacroImplementation.Scope.Static );
-
-        INTERNAL_MACROS = Collections.unmodifiableMap( defaultMacros );
-    }
+        @Override
+        public Set<MacroDefinitionFlag> flags( )
+        {
+            return Collections.emptySet();
+        }
 
-    abstract static class InternalAbstractMacro extends AbstractMacro
-    {
         @Override
-        public MacroDefinitionFlag[] flags( )
+        public Scope getScope()
         {
-            return new MacroDefinitionFlag[]
-                    {
-                            MacroDefinitionFlag.OnlyDebugLogging,
-                    };
+            return Scope.Static;
         }
     }
 
-    public static class PwmSettingReference extends InternalAbstractMacro
+    public static class PwmSettingReference extends StaticAbstractMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@PwmSettingReference" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
 
@@ -83,7 +79,7 @@ public abstract class InternalMacros
         }
 
         @Override
-        public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo )
+        public String replaceValue( final String matchValue, final MacroRequest macroRequestInfo )
                 throws MacroParseException
         {
             final String settingKeyStr = matchValue.substring( 21, matchValue.length() - 1 );
@@ -98,9 +94,15 @@ public abstract class InternalMacros
             }
             return setting.get().toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
         }
+
+        @Override
+        public Set<MacroDefinitionFlag> flags( )
+        {
+            return ONLY_DEBUG_LOG_FLAG;
+        }
     }
 
-    public static class PwmSettingCategoryReference extends InternalAbstractMacro
+    public static class PwmSettingCategoryReference extends StaticAbstractMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@PwmSettingCategoryReference" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
 
@@ -111,7 +113,7 @@ public abstract class InternalMacros
         }
 
         @Override
-        public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo )
+        public String replaceValue( final String matchValue, final MacroRequest macroRequestInfo )
                 throws MacroParseException
         {
             final String settingKeyStr = matchValue.substring( 29, matchValue.length() - 1 );
@@ -124,41 +126,15 @@ public abstract class InternalMacros
 
             return category.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
         }
-    }
-
-    public static class PwmContextPath extends InternalAbstractMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@PwmContextPath@" );
 
         @Override
-        public Pattern getRegExPattern( )
+        public Set<MacroDefinitionFlag> flags( )
         {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo )
-                throws MacroParseException
-        {
-            String contextName = "[context]";
-            final PwmApplication pwmApplication = macroRequestInfo.getPwmApplication();
-            if ( pwmApplication != null )
-            {
-                final PwmEnvironment pwmEnvironment = pwmApplication.getPwmEnvironment();
-                if ( pwmEnvironment != null )
-                {
-                    final ContextManager contextManager = pwmEnvironment.getContextManager();
-                    if ( contextManager != null && contextManager.getContextPath() != null )
-                    {
-                        contextName = contextManager.getContextPath();
-                    }
-                }
-            }
-            return contextName;
+            return ONLY_DEBUG_LOG_FLAG;
         }
     }
 
-    public static class EncodingMacro extends AbstractMacro
+    public static class EncodingMacro extends StaticAbstractMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@Encode:[^:]+:\\[\\[.*\\]\\]@" );
         // @Encode:ENCODE_TYPE:[[value]]@
@@ -217,11 +193,11 @@ public abstract class InternalMacros
         @Override
         public String replaceValue(
                 final String matchValue,
-                final MacroRequestInfo macroRequestInfo
+                final MacroRequest macroRequestInfo
         )
                 throws MacroParseException
         {
-            if ( matchValue == null || matchValue.length() < 1 )
+            if ( StringUtil.isEmpty( matchValue ) )
             {
                 return "";
             }
@@ -248,7 +224,7 @@ public abstract class InternalMacros
         }
     }
 
-    public static class HashingMacro extends AbstractMacro
+    public static class HashingMacro extends StaticAbstractMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@Hash:[^:]+:\\[\\[.*\\]\\]@" );
         // @Hash:HASH_TYPE:[[value]]@
@@ -331,7 +307,7 @@ public abstract class InternalMacros
         @Override
         public String replaceValue(
                 final String matchValue,
-                final MacroRequestInfo macroRequestInfo
+                final MacroRequest macroRequestInfo
         )
                 throws MacroParseException
         {
@@ -362,7 +338,7 @@ public abstract class InternalMacros
         }
     }
 
-    public static class CasingMacro extends AbstractMacro
+    public static class CasingMacro extends StaticAbstractMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@Case:[^:]+:\\[\\[.*\\]\\]@" );
         // @Case:CASE_TYPE:[[value]]@
@@ -421,7 +397,7 @@ public abstract class InternalMacros
         @Override
         public String replaceValue(
                 final String matchValue,
-                final MacroRequestInfo macroRequestInfo
+                final MacroRequest macroRequestInfo
         )
                 throws MacroParseException
         {
@@ -452,7 +428,7 @@ public abstract class InternalMacros
         }
     }
 
-    public static class PwmAppName extends InternalAbstractMacro
+    public static class PwmAppName extends StaticAbstractMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@PwmAppName@" );
 
@@ -463,14 +439,20 @@ public abstract class InternalMacros
         }
 
         @Override
-        public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo )
+        public String replaceValue( final String matchValue, final MacroRequest macroRequestInfo )
                 throws MacroParseException
         {
             return PwmConstants.PWM_APP_NAME;
         }
+
+        @Override
+        public Set<MacroDefinitionFlag> flags( )
+        {
+            return ONLY_DEBUG_LOG_FLAG;
+        }
     }
 
-    public static class PwmVendorName extends InternalAbstractMacro
+    public static class PwmVendorName extends StaticAbstractMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@PwmVendorName@" );
 
@@ -481,10 +463,16 @@ public abstract class InternalMacros
         }
 
         @Override
-        public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo )
+        public String replaceValue( final String matchValue, final MacroRequest macroRequestInfo )
                 throws MacroParseException
         {
             return PwmConstants.PWM_VENDOR_NAME;
         }
+
+        @Override
+        public Set<MacroDefinitionFlag> flags( )
+        {
+            return ONLY_DEBUG_LOG_FLAG;
+        }
     }
 }

+ 418 - 0
server/src/main/java/password/pwm/util/macro/SystemMacros.java

@@ -0,0 +1,418 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 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.util.macro;
+
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmEnvironment;
+import password.pwm.config.PwmSetting;
+import password.pwm.http.ContextManager;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.PwmDateFormat;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.PwmRandom;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class SystemMacros
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( SystemMacros.class );
+
+    static final List<Macro> SYSTEM_MACROS = Collections.unmodifiableList( Stream.of(
+            new CurrentTimeMacro(),
+            new Iso8601DateTimeMacro(),
+            new InstanceIDMacro(),
+            new DefaultEmailFromAddressMacro(),
+            new SiteURLMacro(),
+            new SiteHostMacro(),
+            new RandomCharMacro(),
+            new RandomNumberMacro(),
+            new UUIDMacro(),
+            new PwmContextPath()
+    ).collect( Collectors.toList() ) );
+
+    public abstract static class AbstractSystemMacros extends AbstractMacro
+    {
+        @Override
+        public Scope getScope()
+        {
+            return Scope.System;
+        }
+    }
+
+
+    public static class InstanceIDMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@InstanceID@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            final PwmApplication pwmApplication = request.getPwmApplication();
+
+            if ( pwmApplication == null )
+            {
+                LOGGER.error( request.getSessionLabel(),  () -> "could not replace value for '" + matchValue + "', pwmApplication is null" );
+                return "";
+            }
+
+            return pwmApplication.getInstanceID();
+        }
+    }
+
+    public static class CurrentTimeMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@CurrentTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+
+        )
+                throws MacroParseException
+        {
+            final List<String> parameters = splitMacroParameters( matchValue, "CurrentTime" );
+
+            if ( parameters.size() > 2 )
+            {
+                throw new MacroParseException( "too many parameters" );
+            }
+
+            final PwmDateFormat pwmDateFormat = readDateFormatAndTimeZoneParams( parameters );
+
+            try
+            {
+                return pwmDateFormat.format( Instant.now() );
+            }
+            catch ( final IllegalArgumentException e )
+            {
+                throw new MacroParseException( e.getMessage() );
+            }
+        }
+    }
+
+    public static class Iso8601DateTimeMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@Iso8601" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+
+        )
+                throws MacroParseException
+        {
+            final List<String> parameters = splitMacroParameters( matchValue, "Iso8601" );
+
+            if ( JavaHelper.isEmpty(  parameters ) || parameters.size() != 1 )
+            {
+                throw new MacroParseException( "exactly one parameter is required" );
+            }
+
+            final String param = parameters.get( 0 );
+
+            if ( "DateTime".equalsIgnoreCase( param ) )
+            {
+                return JavaHelper.toIsoDate( Instant.now() );
+            }
+            else if ( "Date".equalsIgnoreCase( param ) )
+            {
+                return Instant.now().atOffset( ZoneOffset.UTC ).format( DateTimeFormatter.ofPattern( "uuuu-MM-dd" ) );
+            }
+            else if ( "Time".equalsIgnoreCase( param ) )
+            {
+                return Instant.now().atOffset( ZoneOffset.UTC ).format( DateTimeFormatter.ofPattern( "HH:mm:ss" ) );
+            }
+
+            throw new MacroParseException( "unknown parameter" );
+        }
+    }
+
+    public static class SiteURLMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@SiteURL@|@Site:URL@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            return request.getPwmApplication().getConfig().readSettingAsString( PwmSetting.PWM_SITE_URL );
+        }
+    }
+
+    public static class DefaultEmailFromAddressMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@DefaultEmailFromAddress@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            return request.getPwmApplication().getConfig().readSettingAsString( PwmSetting.EMAIL_DEFAULT_FROM_ADDRESS );
+        }
+    }
+
+    public static class SiteHostMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@SiteHost@|@Site:Host@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            try
+            {
+                final String siteUrl = request.getPwmApplication().getConfig().readSettingAsString( PwmSetting.PWM_SITE_URL );
+                final URL url = new URL( siteUrl );
+                return url.getHost();
+            }
+            catch ( final MalformedURLException e )
+            {
+                LOGGER.error( () -> "unable to parse configured/detected site URL: " + e.getMessage() );
+            }
+            return "";
+        }
+    }
+
+    public static class RandomCharMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@RandomChar(:[^@]*)?@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+                throws MacroParseException
+        {
+            if ( matchValue == null || matchValue.length() < 1 )
+            {
+                return "";
+            }
+
+            final List<String> parameters = splitMacroParameters( matchValue, "RandomChar" );
+            int length = 1;
+            if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
+            {
+                final int maxLengthPermitted = Integer.parseInt( request.getPwmApplication().getConfig().readAppProperty( AppProperty.MACRO_RANDOM_CHAR_MAX_LENGTH ) );
+                try
+                {
+                    length = Integer.parseInt( parameters.get( 0 ) );
+                    if ( length > maxLengthPermitted )
+                    {
+                        throw new MacroParseException( "maximum permitted length of RandomChar (" + maxLengthPermitted + ") exceeded" );
+                    }
+                    else if ( length <= 0 )
+                    {
+                        throw new MacroParseException( "length of RandomChar (" + maxLengthPermitted + ") must be greater than zero" );
+                    }
+                }
+                catch ( final NumberFormatException e )
+                {
+                    throw new MacroParseException( "error parsing length parameter of RandomChar: " + e.getMessage() );
+                }
+            }
+
+            if ( parameters.size() > 1 && !parameters.get( 1 ).isEmpty() )
+            {
+                final String chars = parameters.get( 1 );
+                return PwmRandom.getInstance().alphaNumericString( chars, length );
+            }
+            else
+            {
+                return PwmRandom.getInstance().alphaNumericString( length );
+            }
+        }
+    }
+
+    public static class RandomNumberMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@RandomNumber(:[^@]*)?@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+                throws MacroParseException
+        {
+            if ( matchValue == null || matchValue.length() < 1 )
+            {
+                return "";
+            }
+
+            final List<String> parameters = splitMacroParameters( matchValue, "RandomNumber" );
+            if ( parameters.size() != 2 )
+            {
+                throw new MacroParseException( "incorrect number of parameter of RandomNumber: "
+                        + parameters.size() + ", should be 2" );
+            }
+
+            final int min;
+            final int max;
+            try
+            {
+                min = Integer.parseInt( parameters.get( 0 ) );
+            }
+            catch ( final NumberFormatException e )
+            {
+                throw new MacroParseException( "error parsing minimum value parameter of RandomNumber: " + e.getMessage() );
+            }
+
+            try
+            {
+                max = Integer.parseInt( parameters.get( 1 ) );
+            }
+            catch ( final NumberFormatException e )
+            {
+                throw new MacroParseException( "error parsing maximum value parameter of RandomNumber: " + e.getMessage() );
+            }
+
+            if ( min > max )
+            {
+                throw new MacroParseException( "minimum value is less than maximum value parameter of RandomNumber" );
+            }
+
+            final int range = max - min;
+            return String.valueOf( PwmRandom.getInstance().nextInt( range ) + min );
+        }
+    }
+
+    public static class UUIDMacro extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@UUID@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            return PwmRandom.getInstance().randomUUID().toString();
+        }
+    }
+
+    public static class PwmContextPath extends AbstractSystemMacros
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@PwmContextPath@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue( final String matchValue, final MacroRequest request )
+                throws MacroParseException
+        {
+            String contextName = "[context]";
+            final PwmApplication pwmApplication = request.getPwmApplication();
+            if ( pwmApplication != null )
+            {
+                final PwmEnvironment pwmEnvironment = pwmApplication.getPwmEnvironment();
+                if ( pwmEnvironment != null )
+                {
+                    final ContextManager contextManager = pwmEnvironment.getContextManager();
+                    if ( contextManager != null && contextManager.getContextPath() != null )
+                    {
+                        contextName = contextManager.getContextPath();
+                    }
+                }
+            }
+            return contextName;
+        }
+    }
+}

+ 480 - 0
server/src/main/java/password/pwm/util/macro/UserMacros.java

@@ -0,0 +1,480 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 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.util.macro;
+
+import password.pwm.AppProperty;
+import password.pwm.bean.LoginInfoBean;
+import password.pwm.bean.UserIdentity;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.UserInfo;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class UserMacros
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( UserMacros.class );
+
+    static final List<Macro> USER_MACROS = Collections.unmodifiableList( Stream.of(
+            new UserIDMacro(),
+            new UserLdapMacro(),
+            new UserPwExpirationTimeMacro(),
+            new UserDaysUntilPwExpireMacro(),
+            new UserEmailMacro(),
+            new UserPasswordMacro(),
+            new UserLdapProfileMacro(),
+            new OtpSetupTimeMacro(),
+            new ResponseSetupTimeMacro()
+    ).collect( Collectors.toList() ) );
+
+    private abstract static class AbstractUserMacro extends AbstractMacro
+    {
+        @Override
+        public Scope getScope()
+        {
+            return Macro.Scope.User;
+        }
+    }
+
+    public static class UserLdapMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@(User:LDAP|LDAP)" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+                throws MacroParseException
+        {
+            final UserInfo userInfo = request.getUserInfo();
+
+            if ( userInfo == null )
+            {
+                return "";
+            }
+
+            final List<String> parameters = splitMacroParameters( matchValue, "User", "LDAP" );
+
+            final String ldapAttr;
+            if ( parameters.size() > 0 && !parameters.get( 0 ).isEmpty() )
+            {
+                ldapAttr = parameters.get( 0 );
+            }
+            else
+            {
+                throw new MacroParseException( "required attribute name parameter is missing" );
+            }
+
+            final int length;
+            if ( parameters.size() > 1 && !parameters.get( 1 ).isEmpty() )
+            {
+                try
+                {
+                    length = Integer.parseInt( parameters.get( 1 ) );
+                }
+                catch ( final NumberFormatException e )
+                {
+                    throw new MacroParseException( "error parsing length parameter: " + e.getMessage() );
+                }
+
+                final int maxLengthPermitted = Integer.parseInt(
+                        request.getPwmApplication() != null
+                                ?  request.getPwmApplication().getConfig().readAppProperty( AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH )
+                                :  AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH.getDefaultValue()
+                );
+
+                if ( length > maxLengthPermitted )
+                {
+                    throw new MacroParseException( "maximum permitted length of LDAP attribute (" + maxLengthPermitted + ") exceeded" );
+                }
+                else if ( length <= 0 )
+                {
+                    throw new MacroParseException( "length parameter must be greater than zero" );
+                }
+            }
+            else
+            {
+                length = 0;
+            }
+
+            final String paddingChar;
+            if ( parameters.size() > 2 && !parameters.get( 2 ).isEmpty() )
+            {
+                paddingChar = parameters.get( 2 );
+            }
+            else
+            {
+                paddingChar = "";
+            }
+
+            if ( parameters.size() > 3 )
+            {
+                throw new MacroParseException( "too many parameters" );
+            }
+
+            final String ldapValue;
+            if ( "dn".equalsIgnoreCase( ldapAttr ) )
+            {
+                ldapValue = userInfo.getUserIdentity().getUserDN();
+            }
+            else
+            {
+                try
+                {
+                    ldapValue = userInfo.readStringAttribute( ldapAttr );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    LOGGER.trace( request.getSessionLabel(), () -> "could not replace value for '" + matchValue + "', ldap error: " + e.getMessage() );
+                    return "";
+                }
+
+                if ( ldapValue == null || ldapValue.length() < 1 )
+                {
+                    LOGGER.trace( request.getSessionLabel(), () -> "could not replace value for '" + matchValue + "', user does not have value for '" + ldapAttr + "'" );
+                    return "";
+                }
+            }
+
+            final StringBuilder returnValue = new StringBuilder();
+            returnValue.append( ldapValue == null
+                    ? ""
+                    : ldapValue );
+
+            if ( length > 0 && length < returnValue.length() )
+            {
+                returnValue.delete( length, returnValue.length() );
+            }
+
+            if ( length > 0 && paddingChar.length() > 0 )
+            {
+                while ( returnValue.length() < length )
+                {
+                    returnValue.append( paddingChar );
+                }
+            }
+
+            return returnValue.toString();
+        }
+    }
+
+
+    public static class UserIDMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:ID@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            final UserInfo userInfo = request.getUserInfo();
+
+            try
+            {
+                if ( userInfo == null || StringUtil.isEmpty( userInfo.getUsername() ) )
+                {
+                    return "";
+                }
+
+                return userInfo.getUsername();
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                LOGGER.error( request.getSessionLabel(), () -> "error reading username during macro replacement: " + e.getMessage() );
+                return "";
+            }
+        }
+    }
+
+    public static class UserPwExpirationTimeMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:PwExpireTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+
+        )
+                throws MacroParseException
+        {
+            final UserInfo userInfo = request.getUserInfo();
+
+            if ( userInfo == null )
+            {
+                return "";
+            }
+
+            final Instant pwdExpirationTime;
+            try
+            {
+                pwdExpirationTime = userInfo.getPasswordExpirationTime();
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                LOGGER.error( request.getSessionLabel(), () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
+                return "";
+            }
+
+            return processTimeOutputMacro( pwdExpirationTime, matchValue, "User", "PwExpireTime" );
+        }
+    }
+
+    public static class UserDaysUntilPwExpireMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:DaysUntilPwExpire@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            final UserInfo userInfo = request.getUserInfo();
+
+            if ( userInfo == null )
+            {
+                LOGGER.error( request.getSessionLabel(), () -> "could not replace value for '" + matchValue + "', userInfoBean is null" );
+                return "";
+            }
+
+            try
+            {
+                final Instant pwdExpirationTime = userInfo.getPasswordExpirationTime();
+                final TimeDuration timeUntilExpiration = TimeDuration.fromCurrent( pwdExpirationTime );
+                final long daysUntilExpiration = timeUntilExpiration.as( TimeDuration.Unit.DAYS );
+                return String.valueOf( daysUntilExpiration );
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                LOGGER.error( request.getSessionLabel(), () -> "error reading pwdExpirationTime during macro replacement: " + e.getMessage() );
+                return "";
+            }
+        }
+    }
+
+    public static class UserEmailMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:Email@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            final UserInfo userInfo = request.getUserInfo();
+
+            try
+            {
+                if ( userInfo == null || userInfo.getUserEmailAddress() == null )
+                {
+                    return "";
+                }
+
+                return userInfo.getUserEmailAddress();
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                LOGGER.error( request.getSessionLabel(), () -> "error reading user email address during macro replacement: " + e.getMessage() );
+                return "";
+            }
+        }
+    }
+
+    public static class UserPasswordMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:Password@" );
+        private static final Set<MacroDefinitionFlag> FLAGS = Collections.singleton( MacroDefinitionFlag.SensitiveValue );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            final LoginInfoBean loginInfoBean = request.getLoginInfoBean();
+
+            try
+            {
+                if ( loginInfoBean == null || loginInfoBean.getUserCurrentPassword() == null )
+                {
+                    return "";
+                }
+
+                return loginInfoBean.getUserCurrentPassword().getStringValue();
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                LOGGER.error( request.getSessionLabel(), () -> "error decrypting in memory password during macro replacement: " + e.getMessage() );
+                return "";
+            }
+        }
+
+        @Override
+        public Set<MacroDefinitionFlag> flags( )
+        {
+            return FLAGS;
+        }
+    }
+
+    public static class UserLdapProfileMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:LdapProfile@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest request
+        )
+        {
+            final UserInfo userInfo = request.getUserInfo();
+
+            if ( userInfo != null )
+            {
+                final UserIdentity userIdentity = userInfo.getUserIdentity();
+                if ( userIdentity != null )
+                {
+                    return userIdentity.getLdapProfileID();
+                }
+            }
+
+            return "";
+        }
+    }
+
+    public static class OtpSetupTimeMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@OtpSetupTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue( final String matchValue, final MacroRequest request )
+                throws MacroParseException
+        {
+            Instant otpSetupTime = null;
+            try
+            {
+                final UserInfo userInfo = request.getUserInfo();
+                if ( userInfo != null && userInfo.getOtpUserRecord() != null && userInfo.getOtpUserRecord().getTimestamp() != null )
+                {
+                    otpSetupTime = userInfo.getOtpUserRecord().getTimestamp();
+                }
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                LOGGER.error( request.getSessionLabel(),  () -> "error reading otp setup time during macro replacement: " + e.getMessage() );
+            }
+
+            return processTimeOutputMacro( otpSetupTime, matchValue, "OtpSetupTime" );
+        }
+    }
+
+    public static class ResponseSetupTimeMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@ResponseSetupTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue( final String matchValue, final MacroRequest request )
+                throws MacroParseException
+        {
+            Instant responseSetupTime = null;
+            try
+            {
+                final UserInfo userInfo = request.getUserInfo();
+                if ( userInfo != null && userInfo.getResponseInfoBean() != null && userInfo.getResponseInfoBean().getTimestamp() != null )
+                {
+                    responseSetupTime = userInfo.getResponseInfoBean().getTimestamp();
+                }
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                LOGGER.error( request.getSessionLabel(), () -> "error reading response setup time macro replacement: " + e.getMessage() );
+            }
+
+            return processTimeOutputMacro( responseSetupTime, matchValue, "ResponseSetupTime" );
+        }
+    }
+}

+ 16 - 16
server/src/main/java/password/pwm/util/operations/ActionExecutor.java

@@ -42,7 +42,7 @@ import password.pwm.util.BasicAuthInfo;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -127,10 +127,10 @@ public class ActionExecutor
             {
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "executor specified macro expansion but did not supply macro machine" ) );
             }
-            final MacroMachine macroMachine = settings.getMacroMachine();
+            final MacroRequest macroRequest = settings.getMacroMachine();
 
-            attributeName = macroMachine.expandMacros( attributeName );
-            attributeValue = macroMachine.expandMacros( attributeValue );
+            attributeName = macroRequest.expandMacros( attributeName );
+            attributeValue = macroRequest.expandMacros( attributeValue );
         }
 
         try
@@ -171,10 +171,10 @@ public class ActionExecutor
                 {
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "executor specified macro expansion but did not supply macro machine" ) );
                 }
-                final MacroMachine macroMachine = settings.getMacroMachine();
+                final MacroRequest macroRequest = settings.getMacroMachine();
 
-                url = macroMachine.expandMacros( url );
-                body = body == null ? "" : macroMachine.expandMacros( body );
+                url = macroRequest.expandMacros( url );
+                body = body == null ? "" : macroRequest.expandMacros( body );
 
                 if ( webAction.getHeaders() != null )
                 {
@@ -184,7 +184,7 @@ public class ActionExecutor
                         final String headerValue = entry.getValue();
                         if ( headerValue != null )
                         {
-                            headers.put( headerName, macroMachine.expandMacros( headerValue ) );
+                            headers.put( headerName, macroRequest.expandMacros( headerValue ) );
                         }
                     }
                 }
@@ -261,7 +261,7 @@ public class ActionExecutor
             final String attrName,
             final String attrValue,
             final ActionConfiguration.LdapMethod ldapMethod,
-            final MacroMachine macroMachine
+            final MacroRequest macroRequest
     )
             throws PwmOperationalException, ChaiUnavailableException
     {
@@ -269,8 +269,8 @@ public class ActionExecutor
                 ? ActionConfiguration.LdapMethod.replace
                 : ldapMethod;
 
-        final String effectiveAttrValue = ( macroMachine != null )
-                ? macroMachine.expandMacros( attrValue )
+        final String effectiveAttrValue = ( macroRequest != null )
+                ? macroRequest.expandMacros( attrValue )
                 : attrValue;
 
 
@@ -344,7 +344,7 @@ public class ActionExecutor
         private final ChaiUser chaiUser;
 
         private boolean expandPwmMacros = true;
-        private MacroMachine macroMachine;
+        private MacroRequest macroRequest;
 
         public ActionExecutorSettings( final PwmApplication pwmApplication, final ChaiUser chaiUser )
         {
@@ -370,9 +370,9 @@ public class ActionExecutor
             return chaiUser;
         }
 
-        private MacroMachine getMacroMachine( )
+        private MacroRequest getMacroMachine( )
         {
-            return macroMachine;
+            return macroRequest;
         }
 
         private UserIdentity getUserIdentity( )
@@ -387,9 +387,9 @@ public class ActionExecutor
         }
 
 
-        public ActionExecutorSettings setMacroMachine( final MacroMachine macroMachine )
+        public ActionExecutorSettings setMacroMachine( final MacroRequest macroRequest )
         {
-            this.macroMachine = macroMachine;
+            this.macroRequest = macroRequest;
             return this;
         }
 

+ 3 - 3
server/src/main/java/password/pwm/util/operations/OtpService.java

@@ -47,7 +47,7 @@ import password.pwm.util.java.JavaHelper;
 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.macro.MacroRequest;
 import password.pwm.util.operations.otp.DbOtpOperator;
 import password.pwm.util.operations.otp.LdapOtpOperator;
 import password.pwm.util.operations.otp.LocalDbOtpOperator;
@@ -165,12 +165,12 @@ public class OtpService implements PwmService
     private List<String> createRawRecoveryCodes( final int numRecoveryCodes, final SessionLabel sessionLabel )
             throws PwmUnrecoverableException
     {
-        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, sessionLabel );
+        final MacroRequest macroRequest = MacroRequest.forNonUserSpecific( pwmApplication, sessionLabel );
         final String configuredTokenMacro = settings.getRecoveryTokenMacro();
         final List<String> recoveryCodes = new ArrayList<>();
         while ( recoveryCodes.size() < numRecoveryCodes )
         {
-            final String code = macroMachine.expandMacros( configuredTokenMacro );
+            final String code = macroRequest.expandMacros( configuredTokenMacro );
             recoveryCodes.add( code );
         }
         return recoveryCodes;

+ 11 - 11
server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java

@@ -40,7 +40,7 @@ import password.pwm.svc.PwmService;
 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.macro.MacroRequest;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -69,7 +69,7 @@ public class PasswordRuleChecks
         private UserInfo userInfo;
         private PasswordRuleReaderHelper ruleHelper;
         private PasswordCharCounter charCounter;
-        private MacroMachine macroMachine;
+        private MacroRequest macroRequest;
     }
 
     private interface RuleChecker
@@ -125,9 +125,9 @@ public class PasswordRuleChecks
         }
 
         final List<ErrorInformation> errorList = new ArrayList<>();
-        final MacroMachine macroMachine = userInfo == null || userInfo.getUserIdentity() == null
-                ? MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL )
-                : MacroMachine.forUser(
+        final MacroRequest macroRequest = userInfo == null || userInfo.getUserIdentity() == null
+                ? MacroRequest.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL )
+                : MacroRequest.forUser(
                 pwmApplication,
                 PwmConstants.DEFAULT_LOCALE,
                 SessionLabel.SYSTEM_LABEL,
@@ -139,7 +139,7 @@ public class PasswordRuleChecks
                 .policy( policy )
                 .userInfo( userInfo )
                 .ruleHelper( policy.getRuleHelper() )
-                .macroMachine( macroMachine )
+                .macroRequest( macroRequest )
                 .charCounter( new PasswordCharCounter( password ) )
                 .build();
 
@@ -542,8 +542,8 @@ public class PasswordRuleChecks
                 {
                     if ( loopValue != null && loopValue.length() > 0 )
                     {
-                        final MacroMachine macroMachine = ruleCheckData.getMacroMachine();
-                        final String expandedValue = macroMachine.expandMacros( loopValue );
+                        final MacroRequest macroRequest = ruleCheckData.getMacroRequest();
+                        final String expandedValue = macroRequest.expandMacros( loopValue );
                         if ( StringUtils.isNotBlank( expandedValue ) )
                         {
                             final String loweredLoop = expandedValue.toLowerCase();
@@ -642,11 +642,11 @@ public class PasswordRuleChecks
                 throws PwmUnrecoverableException
         {
             final List<ErrorInformation> errorList = new ArrayList<>();
-            final MacroMachine macroMachine = ruleCheckData.getMacroMachine();
+            final MacroRequest macroRequest = ruleCheckData.getMacroRequest();
             final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
 
             // check regex matches.
-            for ( final Pattern pattern : ruleHelper.getRegExMatch( macroMachine ) )
+            for ( final Pattern pattern : ruleHelper.getRegExMatch( macroRequest ) )
             {
                 if ( !pattern.matcher( password ).matches() )
                 {
@@ -659,7 +659,7 @@ public class PasswordRuleChecks
             }
 
             // check no-regex matches.
-            for ( final Pattern pattern : ruleHelper.getRegExNoMatch( macroMachine ) )
+            for ( final Pattern pattern : ruleHelper.getRegExNoMatch( macroRequest ) )
             {
                 if ( pattern.matcher( password ).matches() )
                 {

+ 12 - 11
server/src/main/java/password/pwm/util/password/PasswordRuleReaderHelper.java

@@ -27,8 +27,9 @@ import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 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.macro.MacroRequest;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -95,14 +96,14 @@ public class PasswordRuleReaderHelper
         }
     }
 
-    public List<Pattern> getRegExMatch( final MacroMachine macroMachine )
+    public List<Pattern> getRegExMatch( final MacroRequest macroRequest )
     {
-        return readRegExSetting( PwmPasswordRule.RegExMatch, macroMachine );
+        return readRegExSetting( PwmPasswordRule.RegExMatch, macroRequest );
     }
 
-    public List<Pattern> getRegExNoMatch( final MacroMachine macroMachine )
+    public List<Pattern> getRegExNoMatch( final MacroRequest macroRequest )
     {
-        return readRegExSetting( PwmPasswordRule.RegExNoMatch, macroMachine );
+        return readRegExSetting( PwmPasswordRule.RegExNoMatch, macroRequest );
     }
 
     public List<Pattern> getCharGroupValues( )
@@ -138,14 +139,14 @@ public class PasswordRuleReaderHelper
         return StringHelper.convertStrToBoolean( value );
     }
 
-    private List<Pattern> readRegExSetting( final PwmPasswordRule rule, final MacroMachine macroMachine )
+    private List<Pattern> readRegExSetting( final PwmPasswordRule rule, final MacroRequest macroRequest )
     {
         final String input = passwordPolicy.getPolicyMap().get( rule.getKey() );
 
-        return readRegExSetting( rule, macroMachine, input );
+        return readRegExSetting( rule, macroRequest, input );
     }
 
-    public List<Pattern> readRegExSetting( final PwmPasswordRule rule, final MacroMachine macroMachine, final String input )
+    public List<Pattern> readRegExSetting( final PwmPasswordRule rule, final MacroRequest macroRequest, final String input )
     {
         if ( input == null )
         {
@@ -158,13 +159,13 @@ public class PasswordRuleReaderHelper
 
         for ( final String value : values )
         {
-            if ( value != null && value.length() > 0 )
+            if ( !StringUtil.isEmpty( value ) )
             {
                 String valueToCompile = value;
 
-                if ( macroMachine != null && readBooleanValue( PwmPasswordRule.AllowMacroInRegExSetting ) )
+                if ( macroRequest != null && readBooleanValue( PwmPasswordRule.AllowMacroInRegExSetting ) )
                 {
-                    valueToCompile = macroMachine.expandMacros( value );
+                    valueToCompile = macroRequest.expandMacros( value );
                 }
 
                 try

+ 16 - 16
server/src/main/java/password/pwm/util/password/PasswordUtility.java

@@ -86,7 +86,7 @@ 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.macro.MacroRequest;
 import password.pwm.util.operations.ActionExecutor;
 
 import java.io.Serializable;
@@ -122,12 +122,12 @@ public class PasswordUtility
         final String smsNumber = userInfo.getUserSmsNumber();
         String returnToAddress = emailAddress;
 
-        final MacroMachine macroMachine;
+        final MacroRequest macroRequest;
         {
             final LoginInfoBean loginInfoBean = new LoginInfoBean();
             loginInfoBean.setUserCurrentPassword( newPassword );
             loginInfoBean.setUserIdentity( userInfo.getUserIdentity() );
-            macroMachine = MacroMachine.forUser( pwmApplication, null, userInfo, loginInfoBean );
+            macroRequest = MacroRequest.forUser( pwmApplication, null, userInfo, loginInfoBean );
         }
 
 
@@ -136,13 +136,13 @@ public class PasswordUtility
         {
             case SMSONLY:
                 // Only try SMS
-                error = sendNewPasswordSms( userInfo, pwmApplication, macroMachine, newPassword, smsNumber, userLocale );
+                error = sendNewPasswordSms( userInfo, pwmApplication, macroRequest, newPassword, smsNumber, userLocale );
                 returnToAddress = smsNumber;
                 break;
             case EMAILONLY:
             default:
                 // Only try email
-                error = sendNewPasswordEmail( userInfo, pwmApplication, macroMachine, newPassword, emailAddress, userLocale );
+                error = sendNewPasswordEmail( userInfo, pwmApplication, macroRequest, newPassword, emailAddress, userLocale );
                 break;
         }
         if ( error != null )
@@ -155,7 +155,7 @@ public class PasswordUtility
     private static ErrorInformation sendNewPasswordSms(
             final UserInfo userInfo,
             final PwmApplication pwmApplication,
-            final MacroMachine macroMachine,
+            final MacroRequest macroRequest,
             final PasswordData newPassword,
             final String toNumber,
             final Locale userLocale
@@ -173,7 +173,7 @@ public class PasswordUtility
 
         message = message.replace( "%TOKEN%", newPassword.getStringValue() );
 
-        pwmApplication.sendSmsUsingQueue( toNumber, message, null, macroMachine );
+        pwmApplication.sendSmsUsingQueue( toNumber, message, null, macroRequest );
         LOGGER.debug( () -> String.format( "password SMS added to send queue for %s", toNumber ) );
         return null;
     }
@@ -181,7 +181,7 @@ public class PasswordUtility
     private static ErrorInformation sendNewPasswordEmail(
             final UserInfo userInfo,
             final PwmApplication pwmApplication,
-            final MacroMachine macroMachine,
+            final MacroRequest macroRequest,
             final PasswordData newPassword,
             final String toAddress,
             final Locale userLocale
@@ -204,7 +204,7 @@ public class PasswordUtility
         pwmApplication.getEmailQueue().submitEmail(
                 emailItemBean,
                 userInfo,
-                macroMachine );
+                macroRequest );
 
 
         LOGGER.debug( () -> "new password email to " + userInfo.getUserIdentity() + " added to send queue for " + toAddress );
@@ -353,7 +353,7 @@ public class PasswordUtility
                 final LoginInfoBean clonedLoginInfoBean = JsonUtil.cloneUsingJson( pwmSession.getLoginInfoBean(), LoginInfoBean.class );
                 clonedLoginInfoBean.setUserCurrentPassword( newPassword );
 
-                final MacroMachine macroMachine = MacroMachine.forUser(
+                final MacroRequest macroRequest = MacroRequest.forUser(
                         pwmApplication,
                         pwmRequest.getLabel(),
                         pwmSession.getUserInfo(),
@@ -361,7 +361,7 @@ public class PasswordUtility
                 );
 
                 final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userInfo.getUserIdentity() )
-                        .setMacroMachine( macroMachine )
+                        .setMacroMachine( macroRequest )
                         .setExpandPwmMacros( true )
                         .createActionExecutor();
                 actionExecutor.executeActions( actionConfigurations, pwmRequest.getLabel() );
@@ -558,7 +558,7 @@ public class PasswordUtility
                 final LoginInfoBean loginInfoBean = new LoginInfoBean();
                 loginInfoBean.setUserCurrentPassword( newPassword );
 
-                final MacroMachine macroMachine = MacroMachine.forUser(
+                final MacroRequest macroRequest = MacroRequest.forUser(
                         pwmApplication,
                         sessionLabel,
                         userInfo,
@@ -566,7 +566,7 @@ public class PasswordUtility
                 );
 
                 final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
-                        .setMacroMachine( macroMachine )
+                        .setMacroMachine( macroRequest )
                         .setExpandPwmMacros( true )
                         .createActionExecutor();
 
@@ -1247,16 +1247,16 @@ public class PasswordUtility
             return;
         }
 
-        final MacroMachine macroMachine = userInfo == null
+        final MacroRequest macroRequest = userInfo == null
                 ? null
-                : MacroMachine.forUser(
+                : MacroRequest.forUser(
                 pwmApplication,
                 pwmRequest.getLabel(),
                 userInfo,
                 null
         );
 
-        pwmApplication.getEmailQueue().submitEmail( configuredEmailSetting, userInfo, macroMachine );
+        pwmApplication.getEmailQueue().submitEmail( configuredEmailSetting, userInfo, macroRequest );
     }
 
     public static Instant determinePwdLastModified(

+ 3 - 3
server/src/main/java/password/pwm/util/password/PwmPasswordRuleValidator.java

@@ -45,7 +45,7 @@ import password.pwm.util.PasswordData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.ws.client.rest.RestClientHelper;
 
 import java.util.ArrayList;
@@ -237,8 +237,8 @@ public class PwmPasswordRuleValidator
         }
         if ( userInfo != null )
         {
-            final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, userInfo.getUserIdentity() );
-            final PublicUserInfoBean publicUserInfoBean = PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroMachine );
+            final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, userInfo.getUserIdentity() );
+            final PublicUserInfoBean publicUserInfoBean = PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroRequest );
             sendData.put( "userInfo", publicUserInfoBean );
         }
 

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

@@ -38,7 +38,7 @@ 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;
+import password.pwm.util.macro.MacroRequest;
 
 import java.io.Serializable;
 import java.util.LinkedHashMap;
@@ -89,8 +89,8 @@ public class RestTokenDataClient implements RestClient
                     userIdentity, locale
             );
 
-            final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, userInfo.getUserIdentity() );
-            final PublicUserInfoBean publicUserInfoBean = PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), PwmConstants.DEFAULT_LOCALE, macroMachine );
+            final MacroRequest macroRequest = MacroRequest.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, userInfo.getUserIdentity() );
+            final PublicUserInfoBean publicUserInfoBean = PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), PwmConstants.DEFAULT_LOCALE, macroRequest );
             sendData.put( RestClient.DATA_KEY_USERINFO, publicUserInfoBean );
         }
 

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

@@ -46,7 +46,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.FormMap;
 import password.pwm.util.form.FormUtility;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.ws.server.RestMethodHandler;
 import password.pwm.ws.server.RestRequest;
 import password.pwm.ws.server.RestResultBean;
@@ -251,7 +251,7 @@ public class RestProfileServer extends RestServlet
                 targetUserIdentity.getChaiProvider()
         );
 
-        final MacroMachine macroMachine = MacroMachine.forUser(
+        final MacroRequest macroRequest = MacroRequest.forUser(
                 restRequest.getPwmApplication(),
                 restRequest.getLocale(),
                 restRequest.getSessionLabel(),
@@ -263,7 +263,7 @@ public class RestProfileServer extends RestServlet
                 restRequest.getSessionLabel(),
                 restRequest.getLocale(),
                 userInfo,
-                macroMachine,
+                macroRequest,
                 updateProfileProfile,
                 FormUtility.asStringMap( profileFormData ),
                 targetUserIdentity.getChaiUser()

+ 3 - 3
server/src/main/java/password/pwm/ws/server/rest/RestStatusServer.java

@@ -37,7 +37,7 @@ import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.ws.server.RestMethodHandler;
 import password.pwm.ws.server.RestRequest;
 import password.pwm.ws.server.RestResultBean;
@@ -83,7 +83,7 @@ public class RestStatusServer extends RestServlet
                     targetUserIdentity.getUserIdentity(),
                     chaiProvider
             );
-            final MacroMachine macroMachine = MacroMachine.forUser(
+            final MacroRequest macroRequest = MacroRequest.forUser(
                     restRequest.getPwmApplication(),
                     restRequest.getLocale(),
                     restRequest.getSessionLabel(),
@@ -94,7 +94,7 @@ public class RestStatusServer extends RestServlet
                     userInfo,
                     restRequest.getPwmApplication().getConfig(),
                     restRequest.getLocale(),
-                    macroMachine
+                    macroRequest
             );
 
             StatisticsManager.incrementStat( restRequest.getPwmApplication(), Statistic.REST_STATUS );

+ 52 - 53
server/src/test/java/password/pwm/config/profile/PasswordRuleReaderHelperTest.java

@@ -20,86 +20,85 @@
 
 package password.pwm.config.profile;
 
-import org.apache.commons.lang3.StringUtils;
 import org.junit.Assert;
-import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
-import org.mockito.ArgumentMatchers;
-import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-import password.pwm.util.macro.MacroMachine;
+import org.junit.rules.TemporaryFolder;
+import password.pwm.PwmApplication;
+import password.pwm.ldap.UserInfoBean;
+import password.pwm.util.localdb.TestHelper;
+import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.password.PasswordRuleReaderHelper;
 
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Pattern;
 
 public class PasswordRuleReaderHelperTest
 {
-    private static final String[][] MACRO_MAP = new String[][] {
-            {"@User:ID@", "fflintstone"},
-            {"@User:Email@", "fred@flintstones.tv"},
-            {"@LDAP:givenName@", "Fred"},
-            {"@LDAP:sn@", "Flintstone"},
-    };
-
-    private MacroMachine macroMachine = Mockito.mock( MacroMachine.class );
-    private PasswordRuleReaderHelper ruleHelper = Mockito.mock( PasswordRuleReaderHelper.class );
-
-    @Before
-    public void setUp() throws Exception
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    private MacroRequest makeMacroRequest() throws Exception
     {
-        // Mock out things that don't need to be real
-        Mockito.when( macroMachine.expandMacros( ArgumentMatchers.anyString() ) ).thenAnswer( replaceAllMacrosInMap( MACRO_MAP ) );
-        Mockito.when( ruleHelper.readBooleanValue( PwmPasswordRule.AllowMacroInRegExSetting ) ).thenReturn( Boolean.TRUE );
-        Mockito.when( ruleHelper.readRegExSetting(
-                ArgumentMatchers.any( PwmPasswordRule.class ),
-                ArgumentMatchers.any( MacroMachine.class ),
-                ArgumentMatchers.anyString() ) ).thenCallRealMethod();
+        final Map<String, String> userAttributes;
+        {
+            final Map<String, String> map = new HashMap<>();
+            map.put( "cn", "fflintstone" );
+            map.put( "email", "fred@flintstones.tv" );
+            map.put( "givenName", "Fred" );
+            map.put( "sn", "Flintstone" );
+            userAttributes = Collections.unmodifiableMap( map );
+        }
+
+        final PwmApplication pwmApplication = TestHelper.makeTestPwmApplication( temporaryFolder.newFolder() );
+
+        final UserInfoBean userInfo = UserInfoBean.builder()
+                .attributes( userAttributes )
+                .userEmailAddress( "fred@flintstones.tv" )
+                .username( "fflintstone" )
+                .build();
+
+        return  MacroRequest.forUser( pwmApplication, null, userInfo, null );
+    }
+
+    private PasswordRuleReaderHelper makeRuleHelper( final boolean enableMacros )
+    {
+        final Map<String, String> passwordPolicyRules = new HashMap<>( );
+        passwordPolicyRules.put( PwmPasswordRule.AllowMacroInRegExSetting.getKey(), Boolean.toString( enableMacros ) );
+        return new PasswordRuleReaderHelper( PwmPasswordPolicy.createPwmPasswordPolicy( passwordPolicyRules ) );
     }
 
     @Test
     public void testReadRegExSettingNoRegex() throws Exception
     {
+        final MacroRequest macroRequest = makeMacroRequest();
+        final PasswordRuleReaderHelper ruleHelper = makeRuleHelper( true );
+
         final String input = "@User:ID@, First Name: @LDAP:givenName@, Last Name: @LDAP:sn@, Email: @User:Email@";
 
-        final List<Pattern> patterns = ruleHelper.readRegExSetting( PwmPasswordRule.RegExMatch, macroMachine, input );
+        final List<Pattern> patterns = ruleHelper.readRegExSetting( PwmPasswordRule.RegExMatch, macroRequest, input );
 
+        final String expected = "fflintstone, First Name: Fred, Last Name: Flintstone, Email: fred@flintstones.tv";
         Assert.assertEquals( patterns.size(), 1 );
-        Assert.assertEquals( patterns.get( 0 ).pattern(), "fflintstone, First Name: Fred, Last Name: Flintstone, Email: fred@flintstones.tv" );
+        Assert.assertEquals( expected, patterns.get( 0 ).pattern() );
     }
 
     @Test
     public void testReadRegExSetting() throws Exception
     {
+        final MacroRequest macroRequest = makeMacroRequest();
+        final PasswordRuleReaderHelper ruleHelper = makeRuleHelper( true );
+
         final String input = "^@User:ID@[0-9]+$;;;^password$";
 
-        final List<Pattern> patterns = ruleHelper.readRegExSetting( PwmPasswordRule.RegExMatch, macroMachine, input );
+        final List<Pattern> patterns = ruleHelper.readRegExSetting( PwmPasswordRule.RegExMatch, macroRequest, input );
 
         Assert.assertEquals( patterns.size(), 2 );
-        Assert.assertEquals( patterns.get( 0 ).pattern(), "^fflintstone[0-9]+$" );
-        Assert.assertEquals( patterns.get( 1 ).pattern(), "^password$" );
-    }
-
-    private Answer<String> replaceAllMacrosInMap( final String[][] macroMap )
-    {
-        return new Answer<String>()
-        {
-            @Override
-            public String answer( final InvocationOnMock invocation ) throws Throwable
-            {
-                final String[] macroNames = new String[macroMap.length];
-                final String[] macroValues = new String[macroMap.length];
-
-                for ( int i = 0; i < macroMap.length; i++ )
-                {
-                    macroNames[i] = macroMap[i][0];
-                    macroValues[i] = macroMap[i][1];
-                }
-
-                final String stringWithMacros = invocation.getArgument( 0 );
-                return StringUtils.replaceEach( stringWithMacros, macroNames, macroValues );
-            }
-        };
+        Assert.assertEquals( "^fflintstone[0-9]+$", patterns.get( 0 ).pattern() );
+        Assert.assertEquals( "^password$", patterns.get( 1 ).pattern() );
     }
 }

+ 227 - 40
server/src/test/java/password/pwm/util/macro/MacroTest.java

@@ -21,43 +21,60 @@
 package password.pwm.util.macro;
 
 import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
-import org.mockito.Mockito;
-import password.pwm.PwmApplication;
-import password.pwm.PwmApplicationMode;
+import org.junit.rules.TemporaryFolder;
 import password.pwm.PwmConstants;
-import password.pwm.bean.LoginInfoBean;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.Configuration;
-import password.pwm.config.stored.StoredConfigurationFactory;
-import password.pwm.ldap.UserInfo;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.PwmUnrecoverableException;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
 
 public class MacroTest
 {
+    @Rule
+    public TemporaryFolder testFolder = new TemporaryFolder();
+
+    private MacroRequest macroRequest;
+
+    @Before
+    public void setUp() throws PwmUnrecoverableException
+    {
+        macroRequest = MacroRequest.sampleMacroRequest( null );
+    }
+
 
     @Test
     public void testStaticMacros() throws Exception
     {
-        final MacroMachine macroMachine = MacroMachine.forStatic();
-
         // app name
         {
             final String goal = "test " + PwmConstants.PWM_APP_NAME + " test";
-            final String expanded = macroMachine.expandMacros( "test @PwmAppName@ test" );
+            final String expanded = macroRequest.expandMacros( "test @PwmAppName@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+
+        {
+            final String goal = "test " + PwmSetting.PWM_SITE_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) + " test";
+            final String expanded = macroRequest.expandMacros( "test @PwmSettingReference:pwm.selfURL@ test" );
             Assert.assertEquals( goal, expanded );
         }
 
         // urlEncoding macro
         {
             final String goal = "https%3A%2F%2Fwww.example.com";
-            final String expanded = macroMachine.expandMacros( "@Encode:urlPath:[[https://www.example.com]]@" );
+            final String expanded = macroRequest.expandMacros( "@Encode:urlPath:[[https://www.example.com]]@" );
             Assert.assertEquals( goal, expanded );
         }
 
         // base64 macro
         {
             final String goal = "aHR0cHM6Ly93d3cuZXhhbXBsZS5jb20=";
-            final String expanded = macroMachine.expandMacros( "@Encode:base64:[[https://www.example.com]]@" );
+            final String expanded = macroRequest.expandMacros( "@Encode:base64:[[https://www.example.com]]@" );
             Assert.assertEquals( goal, expanded );
         }
     }
@@ -65,78 +82,248 @@ public class MacroTest
     @Test
     public void testStaticHashMacros() throws Exception
     {
-        final MacroMachine macroMachine = MacroMachine.forStatic();
-
         // md5 macro
         {
             final String goal = "f96b697d7cb7938d525a2f31aaf161d0";
-            final String expanded = macroMachine.expandMacros( "@Hash:md5:[[message digest]]@" );
+            final String expanded = macroRequest.expandMacros( "@Hash:md5:[[message digest]]@" );
             Assert.assertEquals( goal, expanded );
         }
 
         // sha1 macro
         {
             final String goal = "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8";
-            final String expanded = macroMachine.expandMacros( "@Hash:sha1:[[password]]@" );
+            final String expanded = macroRequest.expandMacros( "@Hash:sha1:[[password]]@" );
             Assert.assertEquals( goal, expanded );
         }
 
         // sha256 macro
         {
             final String goal = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
-            final String expanded = macroMachine.expandMacros( "@Hash:sha256:[[password]]@" );
+            final String expanded = macroRequest.expandMacros( "@Hash:sha256:[[password]]@" );
             Assert.assertEquals( goal, expanded );
         }
 
         // sha512 macro
         {
             final String goal = "b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86";
-            final String expanded = macroMachine.expandMacros( "@Hash:sha512:[[password]]@" );
+            final String expanded = macroRequest.expandMacros( "@Hash:sha512:[[password]]@" );
+            Assert.assertEquals( goal, expanded );
+        }
+    }
+
+    @Test
+    public void testUserIDMacro() throws Exception
+    {
+        final String goal = "test jrivard test";
+        final String expanded = macroRequest.expandMacros( "test @User:ID@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
+    @Test
+    public void testUserPwExpireTimeMacro()
+    {
+        {
+            final String goal = "UserPwExpireTime 2000-02-03T01:01:01Z test";
+            final String expanded = macroRequest.expandMacros( "UserPwExpireTime @User:PwExpireTime@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "UserPwExpireTime 1:01 AM, UTC test";
+            final String expanded = macroRequest.expandMacros( "UserPwExpireTime @User:PwExpireTime:K/:mm a, z@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "UserPwExpireTime 6:31 AM, IST test";
+            final String expanded = macroRequest.expandMacros( "UserPwExpireTime @User:PwExpireTime:K/:mm a, z:IST@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "UserPwExpireTime 2000.02.03 test";
+            final String expanded = macroRequest.expandMacros( "UserPwExpireTime @User:PwExpireTime:yyyy.MM.dd@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+    }
+
+    @Test
+    public void testUserOtpSetupTimeMacro()
+    {
+        {
+            final String goal = "OtpSetupTime 1999-10-30T04:56:04Z test";
+            final String expanded = macroRequest.expandMacros( "OtpSetupTime @OtpSetupTime@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "OtpSetupTime 4:56 AM, UTC test";
+            final String expanded = macroRequest.expandMacros( "OtpSetupTime @OtpSetupTime:K/:mm a, z@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "OtpSetupTime 10:26 AM, IST test";
+            final String expanded = macroRequest.expandMacros( "OtpSetupTime @OtpSetupTime:K/:mm a, z:IST@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "OtpSetupTime 1999.10.30 test";
+            final String expanded = macroRequest.expandMacros( "OtpSetupTime @OtpSetupTime:yyyy.MM.dd@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+    }
+
+    @Test
+    public void testResponseSetupTimeMacro()
+    {
+        {
+            final String goal = "ResponseSetupTime 1999-10-30T01:17:55Z test";
+            final String expanded = macroRequest.expandMacros( "ResponseSetupTime @ResponseSetupTime@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "ResponseSetupTime 1:17 AM, UTC test";
+            final String expanded = macroRequest.expandMacros( "ResponseSetupTime @ResponseSetupTime:K/:mm a, z@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "ResponseSetupTime 6:47 AM, IST test";
+            final String expanded = macroRequest.expandMacros( "ResponseSetupTime @ResponseSetupTime:K/:mm a, z:IST@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        {
+            final String goal = "ResponseSetupTime 1999.10.30 test";
+            final String expanded = macroRequest.expandMacros( "ResponseSetupTime @ResponseSetupTime:yyyy.MM.dd@ test" );
             Assert.assertEquals( goal, expanded );
         }
     }
 
     @Test
-    public void testUserMacros() throws Exception
+    public void testUserDaysUntilPwExpireMacro()
+            throws PwmUnrecoverableException
+    {
+        final Duration duration = Duration.between( macroRequest.getUserInfo().getPasswordExpirationTime(), Instant.now() );
+        final long days = TimeUnit.DAYS.convert( duration.toMillis(), TimeUnit.MILLISECONDS );
+        final String goal = "UserDaysUntilPwExpire " + days + " test";
+        final String expanded = macroRequest.expandMacros( "UserDaysUntilPwExpire @User:DaysUntilPwExpire@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
+    @Test
+    public void testPasswordMacro()
+    {
+
+        final String goal = "UserPassword PaSSw0rd test";
+        final String expanded = macroRequest.expandMacros( "UserPassword @User:Password@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
+    @Test
+    public void testUserEmailMacro()
+    {
+
+        final String goal = "UserEmail zippy@example.com test";
+        final String expanded = macroRequest.expandMacros( "UserEmail @User:Email@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
+    @Test
+    public void testUserDNMacro()
+    {
+
+        final String goal = "UserDN cn=test1,ou=test,o=org test";
+        final String expanded = macroRequest.expandMacros( "UserDN @LDAP:DN@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
+    @Test
+    public void testUserLdapProfileMacro()
     {
-        final String userDN = "cn=test1,ou=test,o=org";
 
-        final MacroMachine macroMachine;
+        final String goal = "UserLdapProfile profile1 test";
+        final String expanded = macroRequest.expandMacros( "UserLdapProfile @User:LdapProfile@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
+
+    @Test
+    public void testLdapMacro()
+    {
+        // userID macro
         {
-            final PwmApplication pwmApplication = Mockito.mock( PwmApplication.class );
-            Mockito.when( pwmApplication.getApplicationMode() ).thenReturn( PwmApplicationMode.RUNNING );
-            Mockito.when( pwmApplication.getConfig() ).thenReturn( new Configuration( StoredConfigurationFactory.newConfig() ) );
+            final String goal = "cn=test1,ou=test,o=org";
+            final String expanded = macroRequest.expandMacros( "@LDAP:dn@" );
+            Assert.assertEquals( goal, expanded );
+        }
 
-            final UserInfo userInfo = Mockito.mock( UserInfo.class );
-            final UserIdentity userIdentity = new UserIdentity( userDN, "profile" );
-            Mockito.when( userInfo.getUserIdentity() ).thenReturn( userIdentity );
-            Mockito.when( userInfo.readStringAttribute( "givenName" ) ).thenReturn( "Jason" );
 
-            final LoginInfoBean loginInfoBean = Mockito.mock( LoginInfoBean.class );
-            Mockito.when( loginInfoBean.isAuthenticated() ).thenReturn( true );
-            Mockito.when( loginInfoBean.getUserIdentity() ).thenReturn( userIdentity );
+        {
+            final String goal = "test cn%3Dtest1%2Cou%3Dtest%2Co%3Dorg";
+            final String expanded = macroRequest.expandMacros( "test @Encode:urlPath:[[@LDAP:dn@]]@" );
+            Assert.assertEquals( goal, expanded );
+        }
 
-            macroMachine = MacroMachine.forUser( pwmApplication, null, userInfo, loginInfoBean );
+        // user attribute macro
+        {
+            final String goal = "test First test";
+            final String expanded = macroRequest.expandMacros( "test @LDAP:givenName@ test" );
+            Assert.assertEquals( goal, expanded );
         }
 
-        // userDN macro
+        // user attribute with max chars macro
         {
-            final String goal = userDN;
-            final String expanded = macroMachine.expandMacros( "@LDAP:dn@" );
+            final String goal = "test Firs test";
+            final String expanded = macroRequest.expandMacros( "test @LDAP:givenName:4@ test" );
             Assert.assertEquals( goal, expanded );
         }
 
-        // userDN + urlEncoding macro
+        // user attribute with max chars and pad macro
+        {
+            final String goal = "test Firstooooo test";
+            final String expanded = macroRequest.expandMacros( "test @LDAP:givenName:10:o@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+    }
+
+    @Test
+    public void testUserLdapMacro()
+    {
+        // userID macro
+        {
+            final String goal = "cn=test1,ou=test,o=org";
+            final String expanded = macroRequest.expandMacros( "@User:LDAP:dn@" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+
         {
             final String goal = "test cn%3Dtest1%2Cou%3Dtest%2Co%3Dorg";
-            final String expanded = macroMachine.expandMacros( "test @Encode:urlPath:[[@LDAP:dn@]]@" );
+            final String expanded = macroRequest.expandMacros( "test @Encode:urlPath:[[@User:LDAP:dn@]]@" );
             Assert.assertEquals( goal, expanded );
         }
 
         // user attribute macro
         {
-            final String goal = "test Jason test";
-            final String expanded = macroMachine.expandMacros( "test @LDAP:givenName@ test" );
+            final String goal = "test First test";
+            final String expanded = macroRequest.expandMacros( "test @User:LDAP:givenName@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        // user attribute with max chars macro
+        {
+            final String goal = "test Firs test";
+            final String expanded = macroRequest.expandMacros( "test @User:LDAP:givenName:4@ test" );
+            Assert.assertEquals( goal, expanded );
+        }
+
+        // user attribute with max chars and pad macro
+        {
+            final String goal = "test Firstooooo test";
+            final String expanded = macroRequest.expandMacros( "test @User:LDAP:givenName:10:o@ test" );
             Assert.assertEquals( goal, expanded );
         }
     }

+ 2 - 2
webapp/src/main/webapp/WEB-INF/jsp/configguide-start.jsp

@@ -25,7 +25,7 @@
 
 <%@ page import="password.pwm.util.java.JavaHelper" %>
 <%@ page import="password.pwm.util.java.StringUtil" %>
-<%@ page import="password.pwm.util.macro.MacroMachine" %>
+<%@ page import="password.pwm.util.macro.MacroRequest" %>
 
 <% JspUtility.setFlag(pageContext, PwmRequestFlag.HIDE_LOCALE); %>
 <% JspUtility.setFlag(pageContext, PwmRequestFlag.INCLUDE_CONFIG_CSS); %>
@@ -51,7 +51,7 @@
         </p>
         <br/><br/>
         <% String welcomeText = JavaHelper.readEulaText(ContextManager.getContextManager(session),PwmConstants.RESOURCE_FILE_WELCOME_TXT); %>
-        <% String macroText = MacroMachine.forStatic().expandMacros(welcomeText); %>
+        <% String macroText = MacroRequest.forStatic().expandMacros(welcomeText); %>
         <% if (!StringUtil.isEmpty(macroText)) { %>
         <div id="agreementText" class="eulaText"><%=macroText%></div>
         <br/><br/>

+ 3 - 3
webapp/src/main/webapp/public/reference/settings.jsp

@@ -31,7 +31,7 @@
 <%@ page import="password.pwm.util.i18n.LocaleHelper" %>
 <%@ page import="password.pwm.util.java.StringUtil" %>
 <%@ page import="java.util.*" %>
-<%@ page import="password.pwm.util.macro.MacroMachine" %>
+<%@ page import="password.pwm.util.macro.MacroRequest" %>
 <%@ page import="com.novell.ldapchai.util.StringHelper" %>
 <%@ page import="password.pwm.AppProperty" %>
 
@@ -49,7 +49,7 @@
     final PwmRequest pwmRequest = JspUtility.getPwmRequest(pageContext);
     final boolean advancedMode = false;
     final List<PwmSettingCategory> sortedCategories = PwmSettingCategory.valuesForReferenceDoc(userLocale);
-    final MacroMachine macroMachine = MacroMachine.forNonUserSpecific(pwmRequest.getPwmApplication(), pwmRequest.getLabel());
+    final MacroRequest macroRequest = MacroRequest.forNonUserSpecific(pwmRequest.getPwmApplication(), pwmRequest.getLabel());
 %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="/WEB-INF/jsp/fragment/header.jsp" %>
@@ -216,7 +216,7 @@
                 </tr>
                 <tr>
                     <td colspan="2">
-                        <%= macroMachine.expandMacros(setting.getDescription(userLocale)) %>
+                        <%= macroRequest.expandMacros(setting.getDescription(userLocale)) %>
                     </td>
                 </tr>
                 <% } %>

+ 13 - 11
webapp/src/main/webapp/public/resources/js/configeditor.js

@@ -631,6 +631,16 @@ PWM_CFGEDIT.cancelEditing = function() {
 };
 
 PWM_CFGEDIT.showMacroHelp = function() {
+    var processExampleFunction = function() {
+        PWM_MAIN.getObject('panel-testMacroOutput').innerHTML = PWM_MAIN.showString('Display_PleaseWait');
+        var sendData = {};
+        sendData['input'] = PWM_MAIN.getObject('input-testMacroInput').value;
+        var url = "editor?processAction=testMacro";
+        var loadFunction = function(data) {
+            PWM_MAIN.getObject('panel-testMacroOutput').innerHTML = data['data'];
+        };
+        PWM_MAIN.ajaxRequest(url,loadFunction,{content:sendData});
+    };
     require(["dijit/Dialog"],function(Dialog) {
         var idName = 'macroPopup';
         PWM_MAIN.clearDijitWidget(idName);
@@ -641,23 +651,15 @@ PWM_CFGEDIT.showMacroHelp = function() {
             href: PWM_GLOBAL['url-resources'] + "/text/macroHelp.html"
         });
         var attempts = 0;
-        // iframe takes indeterminate amount of time to load, so just retry till it apperas
+        // iframe takes indeterminate amount of time to load, so just retry till it appears
         var loadFunction = function() {
             if (PWM_MAIN.getObject('input-testMacroInput')) {
                 console.log('connected to macroHelpDiv');
                 setTimeout(function(){
                     PWM_MAIN.getObject('input-testMacroInput').focus();
+                    PWM_MAIN.addEventHandler('input-testMacroInput','input',processExampleFunction);
+                    processExampleFunction();
                 },500);
-                PWM_MAIN.addEventHandler('button-testMacro','click',function(){
-                    PWM_MAIN.getObject('panel-testMacroOutput').innerHTML = PWM_MAIN.showString('Display_PleaseWait');
-                    var sendData = {};
-                    sendData['input'] = PWM_MAIN.getObject('input-testMacroInput').value;
-                    var url = "editor?processAction=testMacro";
-                    var loadFunction = function(data) {
-                        PWM_MAIN.getObject('panel-testMacroOutput').innerHTML = data['data'];
-                    };
-                    PWM_MAIN.ajaxRequest(url,loadFunction,{content:sendData});
-                });
             } else {
                 if (attempts < 50) {
                     attempts++;

+ 11 - 5
webapp/src/main/webapp/public/resources/text/macroHelp.html

@@ -35,10 +35,7 @@
             Input
         </td>
         <td>
-            <input maxlength="5000" class="configStringInput" style="width:82%;" id="input-testMacroInput" type="text"/>
-            <button type="button" id="button-testMacro" class="btn">
-                <span class="btn-icon pwm-icon pwm-icon-bolt"></span>Test
-            </button>
+            <input value="Hello @LDAP:givenName@ @LDAP:sn@, Today is @CurrentTime:EEEE:IST@." maxlength="5000" class="configStringInput" style="width:82%;" id="input-testMacroInput" type="text"/>
         </td>
     </tr>
     <tr>
@@ -46,7 +43,7 @@
     </tr>
     </tbody>
 </table>
-<div class="footnote">User-specific macros are resolved using the currently authenticated user (if any)</div>
+<div class="footnote">User-specific macros are resolved using sample data.</div>
 <br/>
 <div style="overflow-x: auto; max-height: 450px">
 <table>
@@ -107,6 +104,15 @@
             Time user's password will expire where <i><span class="documentationParameter">pattern</span></i> is a <a onclick="PWM_CFGEDIT.showDateTimeFormatHelp()" href="#">SimpleDateFormat</a> pattern
         </td>
     </tr>
+    <tr>
+        <td class="key">
+            @User:PwExpireTime:<span class="documentationParameter">pattern:<span class="documentationParameter">tz</span>@
+        </td>
+        <td>
+            Time user's password will expire where <i><span class="documentationParameter">pattern</span></i> is a <a onclick="PWM_CFGEDIT.showDateTimeFormatHelp()" href="#">SimpleDateFormat</a> pattern, and the timezone is a tz specified
+            as a valid <a href="#" onclick="PWM_CFGEDIT.showTimezoneList()">TimeZone ID</a>.
+        </td>
+    </tr>
     <tr>
         <td class="key">
             @User:DaysUntilPwExpire@