package it.mice.voila.runtime.springmvc.csrf;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.util.HtmlUtils;

import it.mice.voila.runtime.exception.BusinessException;
import it.mice.voila.runtime.springmvc.controller.ManageController;
import it.mice.voila.runtime.springmvc.controller.PopupController;
import it.mice.voila.runtime.springmvc.taglibs.TagUtils;
import it.mice.voila.runtime.springmvc.util.SpringMvcUtil;
import it.mice.voila.runtime.util.UserMessage;

/**
 * A Spring MVC <code>HandlerInterceptor</code> which is responsible to enforce CSRF token validity on incoming posts requests. The interceptor
 * should be registered with Spring MVC servlet using the following syntax:
 * <pre>
 *   &lt;mvc:interceptors&gt;
 *        &lt;bean class="it.mice.voila.runtime.springmvc.csrf.CSRFHandlerInterceptor"/&gt;
 *   &lt;/mvc:interceptors&gt;
 *   </pre>
 * @author Eyal Lupu
 * @see CSRFRequestDataValueProcessor
 *
 */
public class CSRFHandlerInterceptor extends HandlerInterceptorAdapter{

    /**
     * Default logger.
     */
    private static final Log LOGGER = LogFactory.getLog(CSRFHandlerInterceptor.class); // NOSONAR

	public static final String SECURITY_CSRF_EXCLUDED_PARAMS = "SECURITY_CSRF_EXCLUDED_PARAMS";

    public static Collection<String> EXCLUDED_PARAMS = new ArrayList<String>();
    private Collection<String> excludedParameters = new ArrayList<String>();
    private boolean disabled = false;
    
	public Collection<String> getExcludedParameters() {
		return excludedParameters;
	}

	public void setExcludedParameters(Collection<String> excludedParameters) {
		this.excludedParameters = excludedParameters;
		EXCLUDED_PARAMS = excludedParameters;
	}
	
	/**
	 * @return the disabled
	 */
	public boolean isDisabled() {
		return disabled;
	}

	/**
	 * @param disabled the disabled to set
	 */
	public void setDisabled(boolean disabled) {
		this.disabled = disabled;
	}

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		if (disabled) {
			return true;
		}
		if (handler instanceof ResourceHttpRequestHandler) {
			return true;
		}
		boolean result = false;
		
		checkXssContent(request);
		
		Object handlerBean = SpringMvcUtil.getHandlerBean(handler);
		if (handlerBean instanceof PopupController) {
			request.setAttribute(TagUtils.SECURITY_SESSION_MODE, "_POPUP");
		} else if (handlerBean instanceof ManageController) {
			request.setAttribute(TagUtils.SECURITY_SESSION_MODE, "_CONTROLLER");
		} else {
			return true;
		}
		if (!request.getMethod().equalsIgnoreCase("POST")) {
			// Not a POST or not a manage controller - allow the request
			result = true;
		} else {
			// This is a POST request - need to check the CSRF token
			String sessionToken = CSRFTokenManager.getTokenForSession(request.getSession());
			String requestToken = CSRFTokenManager.getTokenFromRequest(request);
			if (sessionToken.equals(requestToken)) {
				result = checkParameters(request, response, handler);
			} else {
				throw new BusinessException(new UserMessage("error.security.unauthorizedRequest"));
				//response.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad or missing CSRF value");
				//result = false;
			}
		}
		return result;
	}

	/***
	 * Verifica che il valore contenuto negli excluded parameters sia valido e non contenga attacchi XSS
	 * @param request
	 */
	private void checkXssContent(HttpServletRequest request) {
		String[] params = request.getParameterValues(SECURITY_CSRF_EXCLUDED_PARAMS);
		Enumeration<String> e = request.getParameterNames();
		while (e.hasMoreElements()) {
			String paramName = e.nextElement();
			if (isExcludedParameter(paramName, params)) {
				String paramValue = request.getParameter(paramName);
				checkInvalidCharacters(paramName, paramValue);
			}
		}
	}

	private boolean checkParameters(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {
		
		Collection<String> excludedParametersForRequest = new ArrayList<String>();
		String[] params = request.getParameterValues(SECURITY_CSRF_EXCLUDED_PARAMS);
		SecuritySessionBean ssb = TagUtils.getSecuritySessionBean();
		//TagUtils.removeSecuritySessionBean();
		Enumeration<String> e = request.getParameterNames();
		while (e.hasMoreElements()) {
			String paramName = e.nextElement();
			String paramValue = request.getParameter(paramName);
			if (!CSRFTokenManager.CSRF_PARAM_NAME.equals(paramName) && !isExcludedParameter(paramName, params)) { //FIXME
				FormField ff = ssb.getFormFields().get(paramName);
				if (ff == null) {
					LOGGER.warn("POST request field " + paramName + " not allowed.");
					throw new BusinessException(new UserMessage("error.security.unauthorizedRequest"));
					//response.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad form field parameter");
					//return false;
				} else {
					if (!ff.getParameterValue().isEmpty()) {
						boolean valueCheck = false;
						for (String value : ff.getParameterValue()) {
							if (checkSameValue(paramValue, value, ff.getParameterType(), ff.getActualValue())) {
								valueCheck = true;
								break;
							}
						}
						if (!valueCheck) {
							LOGGER.warn("POST request field " + paramName + " not allowed actual value=\n" + paramValue + "\nPrevious value=\n" + ff.getParameterValue().iterator().next());
							throw new BusinessException(new UserMessage("error.security.unauthorizedRequest"));
							//response.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad form field parameter value");
							//return false;
						}
					}
				}
			}
		}
		return true;
	}

	private void checkInvalidCharacters(String paramName, String paramValue) {
		if ((StringUtils.contains(paramValue, "//") || StringUtils.contains(paramValue, "/*")) && StringUtils.contains(paramValue, "';")) {
			LOGGER.warn("Request field " + paramName + " not allowed value=" + paramValue);
			throw new BusinessException(new UserMessage("error.security.unauthorizedRequest"));
		}
		if ("jumpTo".equals(paramName)) {
			Pattern pattern = Pattern.compile("[a-zA-Z0-9\\-\\_\\@\\.]*");
			Matcher matcher = pattern.matcher(paramValue);

			if (!matcher.matches()) {
				LOGGER.warn("Request field " + paramName + " not allowed value=" + paramValue);
				throw new BusinessException(new UserMessage("error.security.unauthorizedRequest"));
			}
		}				
	}

	private boolean isExcludedParameter(String paramName, String[] params) {
		Collection<String> exs = getExcludedParameters();
		if (params != null) {
			exs.addAll(Arrays.asList(params));
		}
		for (String ex : exs) {
			if (paramName.startsWith(ex)) {
				return true;
			}
		}
		return false;
	}
	
	protected boolean checkSameValue(String paramValue, String savedValue, String type, Object actualValue) {
		paramValue = StringUtils.trimToEmpty(paramValue);
		savedValue = StringUtils.trimToEmpty(savedValue);
		if (paramValue.equals(savedValue)) {
			return true;
		}
		if (HtmlUtils.htmlEscape(paramValue).equals(savedValue)) {
			return true;
		}
		if (actualValue != null) {
			try {
				return new BigDecimal(paramValue).compareTo(new BigDecimal(actualValue.toString())) == 0;
			} catch (Exception e) {
				return false;
			}
		}
		return false;
	}

}
