/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.container.servlet.filters.internal;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.StringUtils;
import org.xwiki.container.servlet.filters.SavedRequestManager;
import org.xwiki.container.servlet.filters.SavedRequestManager.SavedRequest;
/**
* <p>
* A filter that allows requests to be saved and reused later. For example when the current request contains an expired
* authentication token, and the authorization module redirects to the login page, all the information sent by the
* client would be lost; this filter allows to save all that information, and after a successful login, injects the
* saved data in the new request.
* </p>
* <p>
* The saved data is used as a fallback for the new request, meaning that a parameter value is first searched in the new
* request, and only if not found it is searched in the saved request. Only the request parameters are stored, along
* with the request URL needed to verify that the request is reused only in a compatible future request. Multiple
* requests can be stored, each one identified by a distinct ID. A request is restored only if a valid ID was provided
* in the new URL, and if the new URL matches the URL of the saved request (except the query string). A saved session is
* deleted after it is restored, so it cannot be reused more than once.
* </p>
* <p>
* Request data is stored in the current HTTP session, in order to provide a safe temporary storage. The data is only as
* safe as a session is, and it will not be available after the session is invalidated. Another consequence is that only
* HTTP requests are saved.
* </p>
*
* @version $Id: 497f09f8e284d2a251ea7326c89046f1d0bcf61f $
*/
public class SavedRequestRestorerFilter implements Filter
{
/**
* Regular expression used for extracting the SRID from the query string. See
* {@link #getSavedRequest(HttpServletRequest)}.
*/
private static final Pattern SAVED_REQUEST_REGEXP =
Pattern.compile("(?:^|&)" + SavedRequestManager.getSavedRequestIdentifier() + "=([^&]++)");
/**
* The name of the request attribute that specifies if this filter has already been applied to the current request.
* This flag is required to prevent prevent processing the same request multiple times. The value of this request
* attribute is a string. The associated boolean value is determined using {@link Boolean#valueOf(String)}.
*/
private static final String ATTRIBUTE_APPLIED = SavedRequestRestorerFilter.class.getName() + ".applied";
/**
* Request Wrapper that inserts data from a previous request into the current request.
*/
public static class SavedRequestWrapper extends HttpServletRequestWrapper
{
/** The saved request data; may be <code>null</code>, in which case no fallback data is used. */
private SavedRequest savedRequest;
/**
* Simple constructor that forwards the initialization to the default {@link HttpServletRequestWrapper}. No
* saved data is used.
*
* @param request the new request, the primary object wrapped which contains the actual request data.
*/
public SavedRequestWrapper(HttpServletRequest request)
{
super(request);
}
/**
* Constructor that forwards the new request to the {@link HttpServletRequestWrapper}, and stores the saved
* request data internally.
*
* @param newRequest the new request, the primary object wrapped which contains the actual request data.
* @param savedRequest the old request, the secondary object wrapped which contains the saved (fallback) request
* parameters.
*/
public SavedRequestWrapper(HttpServletRequest newRequest, SavedRequest savedRequest)
{
super(newRequest);
this.savedRequest = savedRequest;
}
/**
* Retrieves the value for the parameter, either from the new request, or from the saved data.
*
* @param name the name of the parameter
* @return a <code>String</code> representing the first value of the parameter, or <code>null</code> if no value
* was set in either of the requests.
* @see javax.servlet.ServletRequest#getParameter(java.lang.String)
*/
@Override
public String getParameter(String name)
{
String value = super.getParameter(name);
if (value == null && this.savedRequest != null) {
value = this.savedRequest.getParameter(name);
}
return value;
}
/**
* Retrieves all the values for the parameter, either from the new request, or from the saved data (but not
* combined).
*
* @param name the name of the parameter
* @return an array of <code>String</code> objects containing the parameter's values, or <code>null</code> if no
* value was set in either of the requests.
* @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
*/
@Override
public String[] getParameterValues(String name)
{
String[] values = super.getParameterValues(name);
if (values == null && this.savedRequest != null) {
values = this.savedRequest.getParameterValues(name);
}
return values;
}
/**
* Retrieves the combined map of parameter names - values, with the new values overriding the old ones.
*
* @return an immutable Map containing parameter names as keys and parameter values as map values
* @see javax.servlet.ServletRequest#getParameterMap()
*/
@SuppressWarnings("unchecked")
@Override
public Map<String, String[]> getParameterMap()
{
if (this.savedRequest == null) {
return super.getParameterMap();
} else {
// First put the saved (old) request data in the map, so that the new data overrides it.
Map<String, String[]> map = new HashMap<String, String[]>(this.savedRequest.getParameterMap());
map.putAll(super.getParameterMap());
return Collections.unmodifiableMap(map);
}
}
/**
* Retrieves the combined list of parameter names, from both the new and saved requests.
*
* @return an <code>Enumeration</code> of <code>String</code> objects, each <code>String</code> containing the
* name of a request parameter; or an empty <code>Enumeration</code> if the request has no parameters
* @see javax.servlet.ServletRequest#getParameterNames()
*/
@Override
public Enumeration<String> getParameterNames()
{
return Collections.enumeration(getParameterMap().keySet());
}
}
@Override
public void init(FilterConfig filterConfig)
{
// Don't do anything, as this filter does not need any resources.
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException
{
ServletRequest filteredRequest = request;
// This filter works only for HTTP requests, because they are the only ones with a session.
if (request instanceof HttpServletRequest
&& !Boolean.valueOf((String) request.getAttribute(ATTRIBUTE_APPLIED))) {
// Get the saved request, if any (returns null if not applicable)
SavedRequest savedRequest = getSavedRequest((HttpServletRequest) request);
// Merge the new and the saved request
filteredRequest = new SavedRequestWrapper((HttpServletRequest) request, savedRequest);
filteredRequest.setAttribute(ATTRIBUTE_APPLIED, "true");
}
// Forward the request
chain.doFilter(filteredRequest, response);
// Allow multiple calls to this filter as long as they are not nested.
filteredRequest.removeAttribute(ATTRIBUTE_APPLIED);
}
@Override
public void destroy()
{
// Don't do anything, as this filter does not use any resources.
}
/**
* If this request specifies a saved request (using the srid paramter) and the URL matches the one of the saved
* request, return the SavedRequest and remove it from the session.
*
* @param request the current request
* @return the saved request, if one exists, or <code>null</code>.
*/
@SuppressWarnings("unchecked")
protected SavedRequest getSavedRequest(HttpServletRequest request)
{
// Only do something if the new request contains a Saved Request IDentifier (srid)
String savedRequestId = null;
// Using request.getParameter is not good, since in some containers it prevents using request.getInputStream
// and/or request.getReader. A workaround is to manually extract the srid parameter from the query string, but
// this means that:
// - the srid cannot be used in POST requests, but in all current use cases GET is used anyway;
// - the regular expression used for this is pretty basic, so there might be some URLs that fail to be
// recognized; so far this wasn't observed.
Matcher m = SAVED_REQUEST_REGEXP.matcher(StringUtils.defaultString(request.getQueryString()));
if (m.find()) {
savedRequestId = m.group(1);
}
if (!StringUtils.isEmpty(savedRequestId)) {
// Saved requests are stored in the request session
HttpSession session = request.getSession();
// Get the SavedRequest from the session
Map<String, SavedRequest> savedRequests =
(Map<String, SavedRequest>) session.getAttribute(SavedRequestManager.getSavedRequestKey());
if (savedRequests != null) {
SavedRequest savedRequest = savedRequests.get(savedRequestId);
// Only reuse this request if the new request is for the same resource (URL)
if (savedRequest != null
&& StringUtils.equals(savedRequest.getRequestUrl(), request.getRequestURL().toString())) {
// Remove the saved request from the session
savedRequests.remove(savedRequestId);
// Return the SavedRequest
return savedRequest;
}
}
}
return null;
}
}