All Downloads are FREE. Search and download functionalities are using the official Maven repository.

play.data.validation.ValidationPlugin Maven / Gradle / Ivy

There is a newer version: 2.6.2
Show newest version
package play.data.validation;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyList;
import static play.data.validation.Error.toValidationError;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import net.sf.oval.ConstraintViolation;
import net.sf.oval.context.MethodParameterContext;
import net.sf.oval.guard.Guard;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.PlayPlugin;
import play.exceptions.UnexpectedException;
import play.mvc.ActionInvoker;
import play.mvc.Http;
import play.mvc.Http.Cookie;
import play.mvc.Http.Request;
import play.mvc.Http.Response;
import play.mvc.Scope;
import play.mvc.Scope.RenderArgs;
import play.mvc.Scope.Session;
import play.mvc.results.Result;
import play.utils.ErrorsCookieCrypter;
import play.utils.Java;

@ParametersAreNonnullByDefault
public class ValidationPlugin extends PlayPlugin {

  static final ThreadLocal> keys = new ThreadLocal<>();
  private static final ErrorsCookieCrypter errorsCookieCrypter = new ErrorsCookieCrypter();
  private static final Logger securityLogger = LoggerFactory.getLogger("security");
  private static final Gson GSON = new Gson();
  private static final TypeToken> TYPE_ERRORS_LIST = new TypeToken<>() {};

  @Override
  public void beforeInvocation() {
    keys.set(new HashMap<>());
    Validation.current.set(new Validation());
  }

  @Override
  public void beforeActionInvocation(
      Request request,
      Response response,
      Session session,
      RenderArgs renderArgs,
      Scope.Flash flash,
      Method actionMethod) {
    Validation.current.set(restore(request));
    if (!needsValidation(actionMethod)) {
      return;
    }
    List violations =
        new Validator().validateAction(request, session, actionMethod);
    List errors = new ArrayList<>();
    String[] paramNames = Java.parameterNames(actionMethod);
    for (ConstraintViolation violation : violations) {
      String key =
          paramNames[((MethodParameterContext) violation.getContext()).getParameterIndex()];
      Error error = toValidationError(key, violation);
      errors.add(error);
    }
    Validation.current.get().errors.addAll(errors);
  }

  private boolean needsValidation(Method actionMethod) {
    for (Annotation[] annotations : actionMethod.getParameterAnnotations()) {
      if (annotations.length > 0) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void onActionInvocationResult(
      @Nonnull Request request,
      @Nonnull Response response,
      @Nonnull Session session,
      @Nonnull RenderArgs renderArgs,
      Result result) {
    save(request, response);
  }

  @Override
  public void onActionInvocationException(
      @Nonnull Http.Request request, @Nonnull Response response, @Nonnull Throwable e) {
    clear(response);
  }

  @Override
  public void onActionInvocationFinally(@Nonnull Request request, @Nonnull Response response) {
    onJobInvocationFinally();
  }

  @Override
  public void onJobInvocationFinally() {
    if (keys.get() != null) {
      keys.get().clear();
    }
    keys.remove();
    Validation.current.remove();
  }

  static class Validator extends Guard {

    public List validateAction(
        Http.Request request, Session session, Method actionMethod) {
      Object[] rArgs = ActionInvoker.getActionMethodArgs(request, session, actionMethod);

      List violations = new ArrayList<>();
      violations.addAll(validateMethodParameters(actionMethod, rArgs));
      violations.addAll(validateMethodPre(actionMethod, rArgs));
      return violations;
    }

    private List validateMethodParameters(
        Method actionMethod, Object[] rArgs) {
      InternalValidationCycle cycle1 = new InternalValidationCycle(null, null);
      validateMethodParameters(null, actionMethod, rArgs, cycle1);
      return cycle1.violations;
    }

    private List validateMethodPre(Method actionMethod, Object[] rArgs) {
      InternalValidationCycle cycle2 = new InternalValidationCycle(null, null);
      validateMethodPre(null, actionMethod, rArgs, cycle2);
      return cycle2.violations;
    }
  }

  Validation restore(Request request) {
    try {
      Validation validation = new Validation();
      String cookieName = Scope.COOKIE_PREFIX + "_ERRORS";
      Http.Cookie cookie = request.cookies.get(cookieName);
      if (cookie != null && cookie.value != null && !cookie.value.isBlank()) {
        try {
          String errorsData = errorsCookieCrypter.decrypt(URLDecoder.decode(cookie.value, UTF_8));
          List errors = parseErrorsCookie(errorsData);
          validation.errors.addAll(errors);
        } catch (RuntimeException e) {
          securityLogger.error("Failed to decrypt cookie {}={}", cookieName, cookie.value, e);
        }
      }
      return validation;
    } catch (RuntimeException e) {
      securityLogger.error("Failed to restored validation errors from cookie", e);
      return new Validation();
    }
  }

  @Nonnull
  @CheckReturnValue
  List parseErrorsCookie(String errorsData) {
    try {
      return errorsData.isEmpty() ? emptyList() : GSON.fromJson(errorsData, TYPE_ERRORS_LIST);
    } catch (JsonSyntaxException ignore) {
      return emptyList();
    }
  }

  void save(Request request, @Nullable Response response) {
    if (response == null) {
      // Some request like WebSocket don't have any response
      return;
    }
    if (Validation.errors().isEmpty()) {
      // Only send "delete cookie" header when the cookie was present in the request
      if (request.cookies.containsKey(Scope.COOKIE_PREFIX + "_ERRORS")) {
        response.setCookie(
            Scope.COOKIE_PREFIX + "_ERRORS",
            "",
            null,
            "/",
            0,
            Scope.COOKIE_SECURE,
            Scope.SESSION_HTTPONLY);
      }
      return;
    }
    try {
      String errorsCookieValue = "";
      if (Validation.current() != null && Validation.current().keep) {
        errorsCookieValue = composeErrorsCookieValue(new ArrayList<>(Validation.errors()));
      }
      String errorsData = URLEncoder.encode(errorsCookieCrypter.encrypt(errorsCookieValue), UTF_8);
      response.setCookie(
          Scope.COOKIE_PREFIX + "_ERRORS",
          errorsData,
          null,
          "/",
          null,
          Scope.COOKIE_SECURE,
          Scope.SESSION_HTTPONLY);
    } catch (Exception e) {
      throw new UnexpectedException("Failed to serialize errors cookie", e);
    }
  }

  @Nonnull
  @CheckReturnValue
  String composeErrorsCookieValue(List validationErrors) {
    return GSON.toJson(validationErrors);
  }

  private void clear(@Nonnull Response response) {
    try {
      if (response.cookies != null) {
        Cookie cookie = new Cookie(Scope.COOKIE_PREFIX + "_ERRORS", "");
        cookie.sendOnError = true;
        response.cookies.put(cookie.name, cookie);
      }
    } catch (Exception e) {
      throw new UnexpectedException("Errors serializationProblem", e);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy