com.stormpath.sdk.servlet.mvc.FormController.java Source code

Java tutorial

Introduction

Here is the source code for com.stormpath.sdk.servlet.mvc.FormController.java

Source

/*
 * Copyright 2015 Stormpath, Inc.
 *
 * 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 com.stormpath.sdk.servlet.mvc;

import com.stormpath.sdk.account.Account;
import com.stormpath.sdk.account.AccountStatus;
import com.stormpath.sdk.http.HttpMethod;
import com.stormpath.sdk.lang.Assert;
import com.stormpath.sdk.lang.Collections;
import com.stormpath.sdk.lang.Strings;
import com.stormpath.sdk.provider.ProviderAccountRequest;
import com.stormpath.sdk.provider.ProviderAccountResult;
import com.stormpath.sdk.provider.Providers;
import com.stormpath.sdk.servlet.csrf.CsrfTokenManager;
import com.stormpath.sdk.servlet.csrf.DisabledCsrfTokenManager;
import com.stormpath.sdk.servlet.form.DefaultField;
import com.stormpath.sdk.servlet.form.DefaultForm;
import com.stormpath.sdk.servlet.form.Field;
import com.stormpath.sdk.servlet.form.Form;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static com.stormpath.sdk.servlet.mvc.JacksonFieldValueResolver.MARSHALLED_OBJECT;

/**
 * @since 1.0.RC4
 */
public abstract class FormController extends AbstractController {

    private static final Logger log = LoggerFactory.getLogger(FormController.class);

    public final static String SPRING_SECURITY_AUTHENTICATION_FAILED_KEY = "SPRING_SECURITY_AUTHENTICATION_FAILED_MESSAGE";

    private CsrfTokenManager csrfTokenManager;
    private RequestFieldValueResolver fieldValueResolver;
    private List<Field> formFields;

    public void setCsrfTokenManager(CsrfTokenManager csrfTokenManager) {
        this.csrfTokenManager = csrfTokenManager;
    }

    public void setFieldValueResolver(RequestFieldValueResolver fieldValueResolver) {
        this.fieldValueResolver = fieldValueResolver;
    }

    protected RequestFieldValueResolver getFieldValueResolver() {
        return this.fieldValueResolver;
    }

    public void setFormFields(List<Field> formFields) {
        this.formFields = formFields;
    }

    protected CsrfTokenManager getCsrfTokenManager() {
        return csrfTokenManager;
    }

    @Override
    public void init() throws Exception {
        super.init();
        //Assert.notEmpty(formFields, "formFields cannot be null or empty."); some forms do this manually
        Assert.notNull(csrfTokenManager, "csrfTokenManager cannot be null.");
        Assert.notNull(fieldValueResolver, "fieldValueResolver cannot be null.");
    }

    private boolean isCsrfProtectionEnabled() {
        return csrfTokenManager != null && !(csrfTokenManager instanceof DisabledCsrfTokenManager);
    }

    protected Field createCsrfTokenField(String value) {
        return DefaultField.builder().setName(csrfTokenManager.getTokenName()).setValue(value).setType("hidden")
                .build();
    }

    protected void setCsrfToken(HttpServletRequest request, HttpServletResponse response, Form form)
            throws IllegalArgumentException {
        if (!isJsonPreferred(request, response)) {
            Assert.isInstanceOf(DefaultForm.class, form,
                    "Form implementation class must equal or extend DefaultForm");

            String val = fieldValueResolver.getValue(request, csrfTokenManager.getTokenName());
            if (HttpMethod.POST.name().equalsIgnoreCase(request.getMethod())) {
                //This is a POST so we need to set the submitted CSRF token in the form
                form.addField(createCsrfTokenField(val));
            } else if (HttpMethod.GET.name().equalsIgnoreCase(request.getMethod())) {
                //This is a GET so we need to generate a new CSRF token for the form
                form.addField(createCsrfTokenField(csrfTokenManager.createCsrfToken(request, response)));
            }
        }
    }

    void validateCsrfToken(HttpServletRequest request, HttpServletResponse response, Form form)
            throws IllegalArgumentException {
        if (isCsrfProtectionEnabled() && !isJsonPreferred(request, response)) {
            String csrfToken = form.getFieldValue(csrfTokenManager.getTokenName());
            Assert.isTrue(csrfTokenManager.isValidCsrfToken(request, response, csrfToken), "Invalid CSRF token");
            form.getField(csrfTokenManager.getTokenName())
                    .setValue(csrfTokenManager.createCsrfToken(request, response));
        }
    }

    private void setForm(Map<String, Object> model, Form form) {
        model.put("form", form);
    }

    @Override
    protected ViewModel doGet(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Map<String, ?> model = createModel(request, response);
        return new DefaultViewModel(view, model);
    }

    @SuppressWarnings("unchecked")
    protected Map<String, ?> createModel(HttpServletRequest request, HttpServletResponse response) {
        List<ErrorModel> errors = null;
        // We need to look for error in queryString now that we moved SpringSecurity to in in front of us: https://github.com/stormpath/stormpath-sdk-java/issues/915
        String queryString = request.getQueryString();
        boolean requestContainsError = request.getParameter("error") != null
                || (queryString != null && queryString.contains("error"));
        if (requestContainsError) {
            errors = new ArrayList<>();
            ErrorModel error = null;
            HttpSession session = request.getSession(false);
            // session null check fixes https://github.com/stormpath/stormpath-sdk-java/issues/908
            if (session != null && session.getAttribute(SPRING_SECURITY_AUTHENTICATION_FAILED_KEY) != null) {
                //The login page is being re-rendered after an unsuccessful authentication attempt from Spring Security
                //Fix for https://github.com/stormpath/stormpath-sdk-java/issues/648
                //See StormpathAuthenticationFailureHandler
                error = (ErrorModel) session.getAttribute(SPRING_SECURITY_AUTHENTICATION_FAILED_KEY);
            } else {
                //Fix for https://github.com/stormpath/stormpath-sdk-java/issues/1138
                //TODO This breaks i18n. Fix when Stormpath backend returns specific password policy failure codes.
                error = ErrorModel.builder().setStatus(HttpStatus.SC_BAD_REQUEST)
                        .setMessage(request.getParameter("error")).build();
            }
            errors.add(error);
        }
        return createModel(request, response, null, errors);
    }

    protected Map<String, ?> createModel(HttpServletRequest request, HttpServletResponse response, Form form,
            List<ErrorModel> errors) {

        Map<String, Object> model = newModel();

        if (!Collections.isEmpty(errors)) {
            model.put("errors", errors);
        }

        if (form == null) {
            form = createForm(request, response);
        }

        setForm(model, form);

        String status = Strings.clean(request.getParameter("status"));
        if (status != null) {
            model.put("status", status);
        }

        appendModel(request, response, form, errors, model);

        return model;
    }

    protected Form createForm(HttpServletRequest request, HttpServletResponse response) {
        return createForm(request, response, false);
    }

    protected Form createForm(HttpServletRequest request, HttpServletResponse response, boolean retainPassword) {
        DefaultForm form = DefaultForm.builder().setFields(createFields(request, retainPassword)).build();

        if (isCsrfProtectionEnabled()) {
            setCsrfToken(request, response, form);
        }

        return form;
    }

    protected List<Field> createFields(HttpServletRequest request, boolean retainPassword) {
        List<Field> fields = new ArrayList<>();

        for (Field templateField : formFields) {
            Field clone = templateField.copy();

            if (clone.isEnabled()) {
                String val = fieldValueResolver.getValue(request, clone.getName());
                if (retainPassword && "password".equals(clone.getName())) {
                    clone.setValue(val);
                } else {
                    clone.setValue(val);
                }

                // #645: Allow unresolved i18n keys to pass through for labels and placeholders
                ((DefaultField) clone).setLabel(i18nWithDefault(request, clone.getLabel(), clone.getLabel()));
                ((DefaultField) clone)
                        .setPlaceholder(i18nWithDefault(request, clone.getPlaceholder(), clone.getPlaceholder()));

                fields.add(clone);
            }
        }

        return fields;
    }

    protected void appendModel(HttpServletRequest request, HttpServletResponse response, Form form,
            List<ErrorModel> errors, Map<String, Object> model) {
    }

    protected ViewModel doPost(HttpServletRequest request, HttpServletResponse response) throws Exception {

        Form form = createForm(request, response, true);

        try {
            validate(request, response, form);
            return onValidSubmit(request, response, form);
        } catch (Exception e) {
            return onErrorSubmit(request, response, form, e);
        }
    }

    protected ViewModel onErrorSubmit(HttpServletRequest request, HttpServletResponse response, Form form,
            Exception e) {
        sanitizeForm(form);

        List<ErrorModel> errors = toErrors(request, form, e);

        if (isJsonPreferred(request, response)) {
            //TODO according to the spec if multiple errors only the most relevant should be return in case of JSON response, we don't have way to know that for now
            response.setStatus(errors.get(0).getStatus());
            return new DefaultViewModel(STORMPATH_JSON_VIEW_NAME, errors.get(0).toMap());
        } else {
            Map<String, ?> model = createModel(request, response, form, errors);
            return new DefaultViewModel(view, model);
        }
    }

    private void sanitizeForm(Form form) {
        //do not retain submitted password (not safe to have in the DOM text):
        Field field = form.getField("password");
        if (field != null) {
            field.setValue("");
        }
        field = form.getField("confirmPassword");
        if (field != null) {
            field.setValue("");
        }
    }

    protected abstract List<ErrorModel> toErrors(HttpServletRequest request, Form form, Exception e);

    protected abstract ViewModel onValidSubmit(HttpServletRequest request, HttpServletResponse response, Form form)
            throws Exception;

    protected void validate(HttpServletRequest request, HttpServletResponse response, Form form) {

        validateCsrfToken(request, response, form);

        // check for request body and no parameters
        if (request.getParameterMap().size() == 0 && request.getContentLength() > 0) {
            // Read from request to see if social information exists
            ProviderAccountRequest accountRequest = getAccountProviderRequest(request);
            if (accountRequest != null) {
                ProviderAccountResult result = getApplication(request).getAccount(accountRequest);
                Account account = result.getAccount();
                if (account.getStatus().equals(AccountStatus.ENABLED)) {
                    request.setAttribute(Account.class.getName(), account);
                    return;
                }
            }
        }

        //ensure required fields are present:
        List<Field> fields = form.getFields();
        for (Field field : fields) {
            if (field.isRequired() && field.isEnabled()) {
                String value = form.getFieldValue(field.getName());
                if (value == null) {
                    String key = "stormpath.web." + getControllerKey() + ".form.fields." + field.getName()
                            + ".required";
                    String msg = i18n(request, key);
                    throw new ValidationException(msg);
                }
            }
        }

        //If JSON ensure we are not posting undeclared fields according to the spec
        if (isJsonPreferred(request, response)) {
            Map<String, Object> postedJsonFields = fieldValueResolver.getAllFields(request);

            for (String fieldName : postedJsonFields.keySet()) {
                if (form.getField(fieldName) == null && !"customData".equals(fieldName)) {
                    String key = "stormpath.web.form.fields.unknown";
                    String msg = i18n(request, key, fieldName);
                    throw new ValidationException(msg);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private ProviderAccountRequest getAccountProviderRequest(HttpServletRequest request) {
        Map<String, Object> map = (Map<String, Object>) request.getAttribute(MARSHALLED_OBJECT);
        Map<String, String> providerData = (Map<String, String>) map.get("providerData");

        if (providerData != null) {
            String providerId = providerData.get("providerId");
            ProviderAccountRequest accountRequest = null;
            switch (providerId) {
            case "facebook": {
                String accessToken = providerData.get("accessToken");
                accountRequest = Providers.FACEBOOK.account().setAccessToken(accessToken).build();
                break;
            }
            case "github": {
                String code = providerData.get("code");
                accountRequest = Providers.GITHUB.account()
                        .setAccessToken(exchangeGithubCodeForAccessToken(code, request)).build();
                break;
            }
            case "google": {
                String code = providerData.get("code");
                accountRequest = Providers.GOOGLE.account().setCode(code).build();
                break;
            }
            case "linkedin": {
                String code = providerData.get("code");
                accountRequest = Providers.LINKEDIN.account().setCode(code).build();
                break;
            }
            default: {
                log.error("No provider configured for " + providerId);
            }
            }

            return accountRequest;
        }

        log.debug("Provider data not found in request.");
        return null;
    }
}