/* * 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.wicket.protocol.http; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.text.ParseException; import java.util.ArrayList; import java.util.HashSet; import java.util.Properties; import java.util.Set; import javax.portlet.Portlet; 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.HttpServletResponse; import org.apache.wicket.AbortException; import org.apache.wicket.Application; import org.apache.wicket.RequestContext; import org.apache.wicket.RequestCycle; import org.apache.wicket.Resource; import org.apache.wicket.Session; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.parser.XmlPullParser; import org.apache.wicket.markup.parser.XmlTag; import org.apache.wicket.protocol.http.portlet.FilterRequestContext; import org.apache.wicket.protocol.http.portlet.PortletServletRequestWrapper; import org.apache.wicket.protocol.http.portlet.PortletServletResponseWrapper; import org.apache.wicket.protocol.http.portlet.WicketFilterPortletContext; import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy; import org.apache.wicket.request.RequestParameters; import org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy; import org.apache.wicket.request.target.coding.SharedResourceRequestTargetUrlCodingStrategy; import org.apache.wicket.session.ISessionStore; import org.apache.wicket.settings.IRequestCycleSettings; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.ResourceStreamNotFoundException; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.time.Duration; import org.apache.wicket.util.time.Time; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Filter for initiating handling of Wicket requests. * *
* For 1.3 and onward, what we do is instead of using a servlet, use a filter. * *
* The advantage of a filter is that, unlike a servlet, it can choose not to process the request and
* let whatever is next in chain try. So when using a Wicket filter and a request comes in for
* foo.gif the filter can choose not to process it because it knows it is not a wicket-related
* request. Since the filter didn't process it, it falls on to the application server to try, and
* then it works."
*
* @see WicketServlet for documentation
*
* @author Jonathan Locke
* @author Timur Mehrvarz
* @author Juergen Donnerstag
* @author Igor Vaynberg (ivaynberg)
* @author Al Maw
* @author jcompagner
*/
public class WicketFilter implements Filter
{
/**
* The name of the context parameter that specifies application factory class
*/
public static final String APP_FACT_PARAM = "applicationFactoryClassName";
/**
* The name of the root path parameter that specifies the root dir of the app.
*/
public static final String FILTER_MAPPING_PARAM = "filterMappingUrlPattern";
public static final String FILTER_PATH_ATTR = "org.apache.wicket.filter.path";
/** Log. */
private static final Logger log = LoggerFactory.getLogger(WicketFilter.class);
/**
* Name of parameter used to express a comma separated list of paths that should be ignored
*/
public static final String IGNORE_PATHS_PARAM = "ignorePaths";
/**
* The servlet path holder when the WicketSerlvet is used. So that the filter path will be
* computed with the first request. Note: This variable is by purpose package protected. See
* WicketServlet
*/
static final String SERVLET_PATH_HOLDER = "
* Delegates to {@link WicketFilter#doGet} for actual response rendering.
*
*
* {@link WicketFilter#doFilter} goes through a series of steps of steps to process a request;
*
*
*
* @see WicketFilterPortletContext
* @see PortletServletRequestWrapper
* @see PortletServletResponseWrapper
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
HttpServletRequest httpServletRequest;
HttpServletResponse httpServletResponse;
boolean inPortletContext = false;
if (filterPortletContext != null)
{
// collect the request and response together for convenience handling
FilterRequestContext filterRequestContext = new FilterRequestContext(
(HttpServletRequest)request, (HttpServletResponse)response);
// sets up the FilterRequestContext for this request, such as wrapping the request and
// response objects
inPortletContext = filterPortletContext.setupFilter(getFilterConfig(),
filterRequestContext, getFilterPath((HttpServletRequest)request));
// Retrieve and assign the portlet wrapped request/response objects
httpServletRequest = filterRequestContext.getRequest();
httpServletResponse = filterRequestContext.getResponse();
}
else
{
// assign plane HTTP servlet request/response objects
httpServletRequest = (HttpServletRequest)request;
httpServletResponse = (HttpServletResponse)response;
}
httpServletRequest.setAttribute(FILTER_PATH_ATTR, getFilterPath(httpServletRequest));
// If we are a filter which is only meant to process requests in a portlet context, and we
// are in fact not in a portlet context, stop processing now and pass to next filter in the
// chain.
boolean passToNextFilter = portletOnlyFilter && !inPortletContext;
if (passToNextFilter)
{
chain.doFilter(request, response);
return;
}
final String relativePath = getRelativePath(httpServletRequest);
// check against ignore paths and pass on if a match is found
if (ignorePaths.size() > 0 && relativePath.length() > 0)
{
for (String path : ignorePaths)
{
if (relativePath.startsWith(path))
{
log.debug("Ignoring request {}", httpServletRequest.getRequestURL());
chain.doFilter(request, response);
return;
}
}
}
if (isWicketRequest(relativePath))
{
Application previous = null;
if (Application.exists())
{
previous = Application.get();
}
try
{
// Set the webapplication for this thread
Application.set(webApplication);
// last modified time stamp
long lastModified = getLastModified(httpServletRequest);
if (lastModified == -1)
{
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
boolean requestHandledByWicket = doGet(httpServletRequest, httpServletResponse);
if (requestHandledByWicket == false)
{
chain.doFilter(request, response);
}
}
else
{
long ifModifiedSince;
try
{
ifModifiedSince = httpServletRequest.getDateHeader("If-Modified-Since");
}
catch (IllegalArgumentException e)
{
log.warn("Invalid If-Modified-Since header", e);
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000))
{
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(httpServletResponse, lastModified);
doGet(httpServletRequest, httpServletResponse);
}
else
{
httpServletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
httpServletResponse.setDateHeader("Expires", System.currentTimeMillis() +
Duration.hours(1).getMilliseconds());
}
}
}
finally
{
// unset the application thread local if it didn't exist already.
if (previous == null)
{
Application.unset();
RequestContext.unset();
}
else
{
Application.set(previous);
}
}
}
else
{
// request isn't for us
chain.doFilter(request, response);
}
}
/**
* Handles servlet page requests, delegating to the wicket {@link RequestCycle} system.
*
*
*
*
* @see RequestCycle
* @param servletRequest
* Servlet request object
* @param servletResponse
* Servlet response object
* @return true if the request was handled by wicket, false otherwise
* @throws ServletException
* Thrown if something goes wrong during request handling
* @throws IOException
*/
public boolean doGet(final HttpServletRequest servletRequest,
final HttpServletResponse servletResponse) throws ServletException, IOException
{
String relativePath = getRelativePath(servletRequest);
// Special-case for home page - we redirect to add a trailing slash.
if (relativePath.length() == 0 &&
!Strings.stripJSessionId(servletRequest.getRequestURI()).endsWith("/"))
{
String redirectUrl = servletRequest.getRequestURI() + "/";
String queryString = servletRequest.getQueryString();
if (queryString != null)
{
redirectUrl += "?" + queryString;
}
servletResponse.sendRedirect(servletResponse.encodeRedirectURL(redirectUrl));
return true;
}
final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
final ClassLoader newClassLoader = getClassLoader();
try
{
if (previousClassLoader != newClassLoader)
{
Thread.currentThread().setContextClassLoader(newClassLoader);
}
checkCharacterEncoding(servletRequest);
// Create a new webrequest to wrap the request
final WebRequest request = webApplication.newWebRequest(servletRequest);
// Are we using REDIRECT_TO_BUFFER?
if (webApplication.getRequestCycleSettings().getRenderStrategy() == IRequestCycleSettings.REDIRECT_TO_BUFFER)
{
// Try to see if there is a redirect stored
// try get an existing session
ISessionStore sessionStore = webApplication.getSessionStore();
String sessionId = sessionStore.getSessionId(request, false);
if (sessionId != null)
{
BufferedHttpServletResponse bufferedResponse = null;
String queryString = servletRequest.getQueryString();
// look for buffered response
if (!Strings.isEmpty(queryString))
{
bufferedResponse = webApplication.popBufferedResponse(sessionId,
queryString);
}
else
{
bufferedResponse = webApplication.popBufferedResponse(sessionId,
relativePath);
}
// if a buffered response was found
if (bufferedResponse != null)
{
bufferedResponse.writeTo(servletResponse);
// redirect responses are ignored for the request
// logger...
return true;
}
}
}
// either not REDIRECT_TO_BUFFER or no waiting buffer found - begin the request cycle
WebResponse response = null;
boolean externalCall = !Application.exists();
try
{
// if called externally (i.e. WicketServlet) we need to set the thread local here
// AND clean it up at the end of the request
if (externalCall)
{
Application.set(webApplication);
}
// Create a response object and set the output encoding according to
// wicket's application settings.
response = webApplication.newWebResponse(servletResponse);
response.setAjax(request.isAjax());
response.setCharacterEncoding(webApplication.getRequestCycleSettings()
.getResponseRequestEncoding());
createRequestContext(request, response);
// Create request cycle
final RequestCycle cycle = webApplication.newRequestCycle(request, response);
try
{
// Process request
cycle.request();
return cycle.wasHandled();
}
catch (AbortException e)
{
// noop
}
}
finally
{
// Close response
try
{
if (response != null)
{
response.close();
}
}
catch (Exception e)
{
log.error("closing the buffer error", e);
}
finally
{
// Clean up thread local session
Session.unset();
if (externalCall)
{
// Clean up thread local application if this was an external call
// (if not, doFilter will clean it up)
Application.unset();
RequestContext.unset();
}
}
}
}
finally
{
if (newClassLoader != previousClassLoader)
{
Thread.currentThread().setContextClassLoader(previousClassLoader);
}
}
return true;
}
/**
* Ensures the {@link HttpServletRequest} has the correct character encoding set. Tries to
* intelligently handle the situation where the character encoding information is missing from
* the request.
*
* @param servletRequest
*/
private void checkCharacterEncoding(final HttpServletRequest servletRequest)
{
// If the request does not provide information about the encoding of
// its body (which includes POST parameters), then assume the
// default encoding as defined by the wicket application. Bear in
// mind that the encoding of the request usually is equal to the
// previous response.
// However it is a known bug of IE that it does not provide this
// information. Please see the wiki for more details and why all
// other browser deliberately copied that bug.
if (servletRequest.getCharacterEncoding() == null)
{
try
{
// It this request is a wicket-ajax request, we need decode the
// request always by UTF-8, because the request data is encoded by
// encodeUrlComponent() JavaScript function, which always encode data
// by UTF-8.
String wicketAjaxHeader = servletRequest.getHeader("wicket-ajax");
if (wicketAjaxHeader != null && wicketAjaxHeader.equals("true"))
{
servletRequest.setCharacterEncoding("UTF-8");
}
else
{
// The encoding defined by the wicket settings is used to
// encode the responses. Thus, it is reasonable to assume
// the request has the same encoding. This is especially
// important for forms and form parameters.
servletRequest.setCharacterEncoding(webApplication.getRequestCycleSettings()
.getResponseRequestEncoding());
}
}
catch (UnsupportedEncodingException ex)
{
throw new WicketRuntimeException(ex.getMessage());
}
}
}
/**
* @return The filter config of this WicketFilter
*/
public FilterConfig getFilterConfig()
{
return filterConfig;
}
/**
* Returns a relative path to the filter path and context root from an HttpServletRequest - use
* this to resolve a Wicket request.
*
* @param request
* @return Path requested, minus query string, context path, and filterPath. Relative, no
* leading '/'.
*/
public String getRelativePath(HttpServletRequest request)
{
String path = Strings.stripJSessionId(request.getRequestURI());
String contextPath = request.getContextPath();
path = path.substring(contextPath.length());
if (servletMode)
{
String servletPath = request.getServletPath();
path = path.substring(servletPath.length());
}
filterPath = getFilterPath(request);
if (path.length() > 0)
{
path = path.substring(1);
}
// We should always be under the rootPath, except
// for the special case of someone landing on the
// home page without a trailing slash.
if (!path.startsWith(filterPath))
{
if (filterPath.equals(path + "/"))
{
path += "/";
}
}
if (path.startsWith(filterPath))
{
path = path.substring(filterPath.length());
}
return path;
}
/**
* As per {@link javax.servlet.Filter#init(FilterConfig)}, is called by the web container to
* indicate to a filter that it is being placed into service.
*
* {@link WicketFilter#init(FilterConfig)} goes through a series of steps of steps to
* initialise;
*
*
*
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
public void init(FilterConfig filterConfig) throws ServletException
{
initIgnorePaths(filterConfig);
this.filterConfig = filterConfig;
String filterMapping = filterConfig.getInitParameter(WicketFilter.FILTER_MAPPING_PARAM);
if (SERVLET_PATH_HOLDER.equals(filterMapping))
{
servletMode = true;
}
final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
final ClassLoader newClassLoader = getClassLoader();
try
{
if (previousClassLoader != newClassLoader)
{
Thread.currentThread().setContextClassLoader(newClassLoader);
}
// Try to configure filterPath from web.xml if it's not specified as
// an init-param.
if (filterMapping == null)
{
InputStream is = filterConfig.getServletContext().getResourceAsStream(
"/WEB-INF/web.xml");
if (is != null)
{
try
{
filterPath = getFilterPath(filterConfig.getFilterName(), is);
}
catch (ServletException e)
{
log.error("Error reading servlet/filter path from web.xml", e);
}
catch (SecurityException e)
{
// Swallow this at INFO.
log.info("Couldn't read web.xml to automatically pick up servlet/filter path: " +
e.getMessage());
}
if (filterPath == null)
{
log.info("Unable to parse filter mapping web.xml for " +
filterConfig.getFilterName() + ". " + "Configure with init-param " +
FILTER_MAPPING_PARAM + " if it is not \"/*\".");
}
}
}
webApplicationFactory = getApplicationFactory();
// Construct WebApplication subclass
webApplication = webApplicationFactory.createApplication(this);
// Set this WicketFilter as the filter for the web application
webApplication.setWicketFilter(this);
// Store instance of this application object in servlet context to
// make integration with outside world easier
String contextKey = "wicket:" + filterConfig.getFilterName();
filterConfig.getServletContext().setAttribute(contextKey, webApplication);
// set the application thread local in case initialization code uses it
Application.set(webApplication);
// Call internal init method of web application for default
// initialization
webApplication.internalInit();
// Call init method of web application
webApplication.init();
// We initialize components here rather than in the constructor or
// in the internal init, because in the init method class aliases
// can be added, that would be used in installing resources in the
// component.
webApplication.initializeComponents();
// Give the application the option to log that it is started
webApplication.logStarted();
portletOnlyFilter = Boolean.valueOf(filterConfig.getInitParameter(PORTLET_ONLY_FILTER))
.booleanValue();
// sets up Portlet context if this application is deployed as a portlet
if (isPortletContextAvailable(filterConfig))
{
filterPortletContext = newWicketFilterPortletContext();
}
// if WicketFilterPortletContext instantiation succeeded, initialise it
if (filterPortletContext != null)
{
filterPortletContext.initFilter(filterConfig, webApplication);
}
}
finally
{
Application.unset();
// restore the class loader if it was replaced
if (newClassLoader != previousClassLoader)
{
Thread.currentThread().setContextClassLoader(previousClassLoader);
}
}
}
/**
* initializes the ignore paths parameter
*
* @param filterConfig
*/
private void initIgnorePaths(FilterConfig filterConfig)
{
String paths = filterConfig.getInitParameter(IGNORE_PATHS_PARAM);
if (!Strings.isEmpty(paths))
{
String[] parts = paths.split(",");
for (String path : parts)
{
if (path.startsWith("/"))
{
path = path.substring(1);
}
ignorePaths.add(path);
}
}
}
/**
* Tries to find if a PortletContext is available. Searches for the 'detect portlet context'
* flag in various places and if true, tries to load the {@link javax.portlet.PortletContext}.
*
* @param config
* the FilterConfig object
* @return true if {@link javax.portlet.PortletContext} was successfully loaded
* @throws ServletException
* on IO errors
*/
protected boolean isPortletContextAvailable(FilterConfig config) throws ServletException
{
boolean detectPortletContext = false;
// search for portlet detection boolean in various places
String parameter = config.getInitParameter(DETECT_PORTLET_CONTEXT);
// search filter parameter
if (parameter != null)
{
detectPortletContext = Boolean.valueOf(parameter).booleanValue();
}
else
{
parameter = config.getServletContext().getInitParameter(
DETECT_PORTLET_CONTEXT_FULL_NAME);
// search web.xml context paramter
if (parameter != null)
{
detectPortletContext = Boolean.valueOf(parameter).booleanValue();
}
else
{
InputStream is = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream(WICKET_PORTLET_PROPERTIES);
// search wicket.properties
if (is != null)
{
try
{
Properties properties = new Properties();
properties.load(is);
detectPortletContext = Boolean.valueOf(
properties.getProperty(DETECT_PORTLET_CONTEXT_FULL_NAME, "false"))
.booleanValue();
}
catch (IOException e)
{
throw new ServletException(
"Failed to load WicketPortlet.properties from classpath", e);
}
}
}
}
if (detectPortletContext)
{
// load the portlet context
try
{
Class.forName("javax.portlet.PortletContext");
return true;
}
catch (ClassNotFoundException e)
{
}
}
return false;
}
protected WicketFilterPortletContext newWicketFilterPortletContext()
{
return new WicketFilterPortletContext();
}
protected void createRequestContext(WebRequest request, WebResponse response)
{
if (filterPortletContext == null ||
!filterPortletContext.createPortletRequestContext(request, response))
{
new RequestContext();
}
}
private String getFilterPath(String filterName, InputStream is) throws ServletException
{
String prefix = servletMode ? "servlet" : "filter";
String mapping = prefix + "-mapping";
String name = prefix + "-name";
// Filter mappings look like this:
//
//