/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.util.macro;
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.bean.UserInfoBean;
import password.pwm.config.PwmSetting;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.http.PwmRequest;
import password.pwm.ldap.LdapUserDataReader;
import password.pwm.ldap.UserDataReader;
import password.pwm.ldap.UserStatusReader;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.StringUtil;
import password.pwm.util.logging.PwmLogger;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MacroMachine {
private static final PwmLogger LOGGER = PwmLogger.forClass(MacroMachine.class);
private final PwmApplication pwmApplication;
private final SessionLabel sessionLabel;
private final UserInfoBean userInfoBean;
private final LoginInfoBean loginInfoBean;
private final UserDataReader userDataReader;
private static final Map<MacroImplementation.Scope,Map<Pattern,MacroImplementation>> BUILTIN_MACROS = makeImplementations();
public MacroMachine(
final PwmApplication pwmApplication,
final SessionLabel sessionLabel,
final UserInfoBean userInfoBean,
final LoginInfoBean loginInfoBean,
final UserDataReader userDataReader
)
{
this.pwmApplication = pwmApplication;
this.sessionLabel = sessionLabel;
this.userInfoBean = userInfoBean;
this.loginInfoBean = loginInfoBean;
this.userDataReader = userDataReader;
}
private static Map<MacroImplementation.Scope,Map<Pattern,MacroImplementation>> 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 Class macroClass : implementations.keySet()) {
final MacroImplementation.Scope scope = implementations.get(macroClass);
try {
final MacroImplementation macroImplementation = (MacroImplementation)macroClass.newInstance();
final Pattern pattern = macroImplementation.getRegExPattern();
if (!map.containsKey(scope)) {
map.put(scope, new LinkedHashMap<Pattern, MacroImplementation>());
}
map.get(scope).put(pattern,macroImplementation);
} catch (Exception e) {
LOGGER.error("unable to load macro class " + macroClass.getName() + ", error: " + e.getMessage());
}
}
return map;
}
private Map<Pattern,MacroImplementation> makeExternalImplementations(final PwmApplication pwmApplication) {
final LinkedHashMap<Pattern,MacroImplementation> map = new LinkedHashMap<>();
final List<String> externalMethods = (pwmApplication == null)
? Collections.emptyList()
: pwmApplication.getConfig().readSettingAsStringArray(PwmSetting.EXTERNAL_MACROS_REST_URLS);
int iteration = 0;
for (final String url : externalMethods) {
iteration++;
final MacroImplementation macroImplementation = new ExternalRestMacro(iteration,url);
final Pattern pattern = macroImplementation.getRegExPattern();
map.put(pattern,macroImplementation);
}
return map;
}
public String expandMacros(
final String input
) {
return expandMacros(input, null);
}
public String expandMacros(
final String input,
final StringReplacer stringReplacer
)
{
if (input == null) {
return null;
}
if (input.length() < 1) {
return input;
}
final MacroImplementation.MacroRequestInfo macroRequestInfo = new MacroImplementation.MacroRequestInfo() {
@Override
public PwmApplication getPwmApplication()
{
return pwmApplication;
}
@Override
public UserInfoBean getUserInfoBean()
{
return userInfoBean;
}
@Override
public LoginInfoBean getLoginInfoBean()
{
return loginInfoBean;
}
@Override
public UserDataReader getUserDataReader()
{
return userDataReader;
}
};
final Set<MacroImplementation.Scope> scopes = effectiveScopes(macroRequestInfo);
final Map<Pattern,MacroImplementation> macroImplementations = new LinkedHashMap<>();
//First the User macros
if (scopes.contains(MacroImplementation.Scope.User)) {
macroImplementations.putAll(makeExternalImplementations(pwmApplication));
}
//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;
for (final Pattern pattern : macroImplementations.keySet()) {
final MacroImplementation pwmMacro = macroImplementations.get(pattern);
boolean matched = true;
while (matched) {
final Matcher matcher = pattern.matcher(workingString);
if (matcher.find()) {
workingString = doReplace(workingString, pwmMacro, matcher, stringReplacer, 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;
}
}
}
return workingString;
}
private static Set<MacroImplementation.Scope> effectiveScopes(final MacroImplementation.MacroRequestInfo macroRequestInfo) {
final Set<MacroImplementation.Scope> scopes = new HashSet<>();
scopes.add(MacroImplementation.Scope.Static);
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) {
scopes.add(MacroImplementation.Scope.System);
if (macroRequestInfo.getUserInfoBean() != null || macroRequestInfo.getUserDataReader() != null) {
scopes.add(MacroImplementation.Scope.User);
}
}
return Collections.unmodifiableSet(scopes);
}
private String doReplace(
final String input,
final MacroImplementation macroImplementation,
final Matcher matcher,
final StringReplacer stringReplacer,
final MacroImplementation.MacroRequestInfo macroRequestInfo
) {
final String matchedStr = matcher.group();
final int startPos = matcher.start();
final int endPos = matcher.end();
String replaceStr = "";
try {
replaceStr = macroImplementation.replaceValue(matchedStr, macroRequestInfo);
} catch (MacroParseException e) {
LOGGER.debug(sessionLabel, "macro parse error replacing macro '" + matchedStr + "', error: " + e.getMessage());
if (pwmApplication != null) {
replaceStr = "[" + e.getErrorInformation().toUserStr(PwmConstants.DEFAULT_LOCALE, macroRequestInfo.getPwmApplication().getConfig()) + "]";
} else {
replaceStr = "[" + e.getErrorInformation().toUserStr(PwmConstants.DEFAULT_LOCALE, null) + "]";
}
} catch (Exception e) {
LOGGER.error(sessionLabel, "error while replacing macro '" + matchedStr + "', error: " + e.getMessage());
}
if (replaceStr == null) {
return input;
}
if (stringReplacer != null) {
try {
replaceStr = stringReplacer.replace(matchedStr, replaceStr);
} catch (Exception e) {
LOGGER.error(sessionLabel,"unexpected error while executing '" + matchedStr + "' during StringReplacer.replace(), error: " + e.getMessage());
}
}
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);
if (!debugOnlyLogging || (pwmApplication != null && pwmApplication.getConfig().isDevDebugMode())) {
LOGGER.trace(sessionLabel, "replaced macro " + matchedStr + " with value: "
+ (sensitive ? PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT : replaceStr));
}
}
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 class URLEncoderReplacer implements StringReplacer {
public String replace(final String matchedMacro, final String newValue) {
return StringUtil.urlEncode(newValue); // make sure replacement values are properly encoded
}
}
public static MacroMachine forUser(
final PwmRequest pwmRequest,
final UserIdentity userIdentity
)
throws PwmUnrecoverableException
{
return forUser(pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getSessionLabel(),userIdentity);
}
public static MacroMachine forUser(
final PwmApplication pwmApplication,
final Locale userLocale,
final SessionLabel sessionLabel,
final UserIdentity userIdentity
)
throws PwmUnrecoverableException
{
final UserDataReader userDataReader = LdapUserDataReader.appProxiedReader(pwmApplication, userIdentity);
final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication, sessionLabel);
final UserInfoBean userInfoBean = userStatusReader.populateUserInfoBean(userLocale, userIdentity);
return new MacroMachine(pwmApplication, sessionLabel, userInfoBean, null, userDataReader);
}
public static MacroMachine forNonUserSpecific(
final PwmApplication pwmApplication,
final SessionLabel sessionLabel
)
throws PwmUnrecoverableException
{
return new MacroMachine(pwmApplication, sessionLabel, null, null, null);
}
}