PostbackResult.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.struts2.result;

import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
import org.apache.struts2.dispatcher.mapper.ActionMapping;

import java.io.PrintWriter;
import java.io.Serial;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * <!-- START SNIPPET: description -->
 * <p>
 * A result that renders the current request parameters as a form which
 * immediately submits a <a href="http://en.wikipedia.org/wiki/Postback">postback</a>
 * to the specified destination.
 * </p>
 * <!-- END SNIPPET: description -->
 * <p>
 * <b>Parameters:</b>
 * </p>
 * <!-- START SNIPPET: params -->
 * <ul>
 *     <li>location - http location to post the form</li>
 *     <li>prependServletContext (true|false) -  when location is relative, controls if to add Servlet Context, default "true"</li>
 *     <li>actionName - action name to post the form (resolved as an expression)</li>
 *     <li>namespace - action's namespace to use (resolved as an expression)</li>
 *     <li>method - actions' method to use (resolved as an expression)</li>
 *     <li>cache (true|false) - when set to true adds cache control headers, default "true"</li>
 *     <li>parse (true|false) - when set to true actionName, namespace and method are parsed, default "true"</li>
 * </ul>
 * <!-- END SNIPPET: params -->
 * <p>
 * <b>Examples:</b>
 * </p>
 * <pre>
 * <!-- START SNIPPET: example -->
 * &lt;action name="registerThirdParty" &gt;
 *   &lt;result type="postback"&gt;https://www.example.com/register&lt;/result&gt;
 * &lt;/action&gt;
 *
 * &lt;action name="registerThirdParty" &gt;
 *   &lt;result type="postback"&gt;
 *     &lt;param name="namespace"&gt;/secure&lt;/param&gt;
 *     &lt;param name="actionName"&gt;register2&lt;/param&gt;
 *   &lt;/result&gt;
 * &lt;/action&gt;
 * <!-- END SNIPPET: example -->
 * </pre>
 */
public class PostbackResult extends StrutsResultSupport {

    @Serial
    private static final long serialVersionUID = -2283504349296877429L;

    private String actionName;
    private String namespace;
    private String method;
    private boolean prependServletContext = true;
    private boolean cache = true;

    protected ActionMapper actionMapper;

    @Override
    protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
        ActionContext ctx = invocation.getInvocationContext();
        HttpServletRequest request = ctx.getServletRequest();
        HttpServletResponse response = ctx.getServletResponse();

        // Cache?
        if (!cache) {
            response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1
            response.setHeader("Pragma", "no-cache"); // HTTP 1.0
            response.setDateHeader("Expires", 0); // Proxies
        }

        //set contenttype @see ww-4564
        response.setContentType("text/html");

        // Render
        PrintWriter pw = new PrintWriter(response.getOutputStream());
        pw.write("<!DOCTYPE html><html><body><form action=\"" + finalLocation + "\" method=\"POST\">");
        writeFormElements(request, pw);
        writePrologueScript(pw);
        pw.write("</html>");
        pw.flush();
    }

    @Override
    public void execute(ActionInvocation invocation) throws Exception {
        if (invocation == null) {
            throw new IllegalArgumentException("Invocation cannot be null!");
        }

        String postbackUri = makePostbackUri(invocation);
        setLocation(postbackUri);
        super.execute(invocation);
    }

    /**
     * Determines if the specified form input element should be included.
     *
     * @param name   the input element name
     * @param values the input element values
     * @return {@code true} if included; otherwise {@code false}
     */
    protected boolean isElementIncluded(String name, String[] values) {
        return !name.startsWith("action:");
    }

    protected String makePostbackUri(ActionInvocation invocation) {
        ActionContext ctx = invocation.getInvocationContext();
        HttpServletRequest request = ctx.getServletRequest();
        String postbackUri;

        if (actionName != null) {
            actionName = conditionalParse(actionName, invocation);
            parseLocation = false;
            if (namespace == null) {
                namespace = invocation.getProxy().getNamespace();
            } else {
                namespace = conditionalParse(namespace, invocation);
            }
            if (method == null) {
                method = "";
            } else {
                method = conditionalParse(method, invocation);
            }
            postbackUri = request.getContextPath() + actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null));
        } else {
            String location = getLocation();
            // Do not prepend if the URL is a FQN
            if (!location.matches("^([a-zA-Z]+:)?//.*")) {
                // If the URL is relative to the servlet context, prepend the servlet context path
                if (prependServletContext && request.getContextPath() != null && !request.getContextPath().isEmpty()) {
                    location = request.getContextPath() + location;
                }
            }
            postbackUri = location;
        }

        return postbackUri;
    }

    @Inject
    public final void setActionMapper(ActionMapper mapper) {
        this.actionMapper = mapper;
    }

    /**
     * Sets the name of the destination action.
     *
     * @param actionName the action name
     */
    public final void setActionName(String actionName) {
        this.actionName = actionName;
    }

    /**
     * Stores the option to cache the rendered intermediate page. The default
     * is {@code true}.
     *
     * @param cache enable/disable cache
     */
    public final void setCache(boolean cache) {
        this.cache = cache;
    }

    /**
     * Sets the method of the destination action.
     *
     * @param method the method
     */
    public final void setMethod(String method) {
        this.method = method;
    }

    /**
     * Sets the namespace of the destination action.
     *
     * @param namespace the namespace
     */
    public final void setNamespace(String namespace) {
        this.namespace = namespace;
    }

    public final void setPrependServletContext(boolean prependServletContext) {
        this.prependServletContext = prependServletContext;
    }

    protected void writeFormElement(PrintWriter pw, String name, String[] values) throws UnsupportedEncodingException {
        for (String value : values) {
            String encName = URLEncoder.encode(name, StandardCharsets.UTF_8);
            String encValue = URLEncoder.encode(value, StandardCharsets.UTF_8);
            pw.write("<input type=\"hidden\" name=\"" + encName + "\" value=\"" + encValue + "\"/>");
        }
    }

    private void writeFormElements(HttpServletRequest request, PrintWriter pw) throws UnsupportedEncodingException {
        Map<String, String[]> params = request.getParameterMap();
        for (Map.Entry<String, String[]> entry : params.entrySet()) {
            String name = entry.getKey();
            String[] values = entry.getValue();
            if (isElementIncluded(name, values)) {
                writeFormElement(pw, name, values);
            }
        }
    }

    /**
     * Outputs the script after the form has been emitted. The default script
     * is to submit the form using a JavaScript time out that immediately expires.
     *
     * @param pw the print writer
     */
    protected void writePrologueScript(PrintWriter pw) {
        pw.write("<script>");
        pw.write("setTimeout(function(){document.forms[0].submit();},0);");
        pw.write("</script>");
    }

}