/* * 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.deltaspike.jsf.impl.scope.window.strategy; import java.io.IOException; import java.io.OutputStream; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Typed; import javax.faces.FacesException; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import org.apache.deltaspike.jsf.impl.util.ClientWindowHelper; import org.apache.deltaspike.jsf.impl.util.JsfUtils; @Dependent @Typed(ClientSideWindowStrategy.class) public class ClientSideWindowStrategy extends AbstractClientWindowStrategy { /** * Value which can be used as "window-id" by external clients which aren't aware of windows. * It deactivates e.g. the redirect for the initial request. */ private static final String AUTOMATED_ENTRY_POINT_PARAMETER_KEY = "automatedEntryPoint"; private static final String UNINITIALIZED_WINDOW_ID_VALUE = "uninitializedWindowId"; private static final String WINDOW_ID_REPLACE_PATTERN = "$$windowIdValue$$"; private static final String REQUEST_URL_REPLACE_PATTERN = "$$requestUrl$$"; private static final String NOSCRIPT_URL_REPLACE_PATTERN = "$$noscriptUrl$$"; /** * Use this parameter to force a 'direct' request from the clients without any windowId detection * We keep this name for backward compat with CODI. */ private static final String NOSCRIPT_PARAMETER = "mfDirect"; @Override protected String getOrCreateWindowId(FacesContext facesContext) { String windowId = null; boolean post = isPost(facesContext); if (post) { windowId = getWindowIdPostParameter(facesContext); } else if (isNoscriptRequest(facesContext.getExternalContext())) { // the client has JavaScript disabled clientWindowConfig.setJavaScriptEnabled(false); windowId = DEFAULT_WINDOW_ID; } else { windowId = getVerifiedWindowIdFromCookie(facesContext.getExternalContext()); boolean newWindowIdRequested = false; if (AUTOMATED_ENTRY_POINT_PARAMETER_KEY.equals(windowId)) { // this is a marker for generating a new windowId windowId = generateNewWindowId(); newWindowIdRequested = true; } if (windowId == null || newWindowIdRequested) { // GET request without windowId - send windowhandlerfilter.html to get the windowId sendWindowHandlerHtml(facesContext.getExternalContext(), windowId); facesContext.responseComplete(); } } return windowId; } protected boolean isNoscriptRequest(ExternalContext externalContext) { String noscript = externalContext.getRequestParameterMap().get(NOSCRIPT_PARAMETER); return (noscript != null && "true".equals(noscript)); } protected void sendWindowHandlerHtml(ExternalContext externalContext, String windowId) { HttpServletResponse httpResponse = (HttpServletResponse) externalContext.getResponse(); try { httpResponse.setStatus(HttpServletResponse.SC_OK); httpResponse.setContentType("text/html"); String windowHandlerHtml = clientWindowConfig.getClientWindowHtml(); if (windowId == null) { windowId = UNINITIALIZED_WINDOW_ID_VALUE; } // set the windowId value in the javascript code windowHandlerHtml = windowHandlerHtml.replace(WINDOW_ID_REPLACE_PATTERN, windowId); // set the current request url // on the client we can't use window.location as the location // could be a different when using forwards windowHandlerHtml = windowHandlerHtml.replace(REQUEST_URL_REPLACE_PATTERN, ClientWindowHelper.constructRequestUrl(externalContext)); // set the noscript-URL for users with no JavaScript windowHandlerHtml = windowHandlerHtml.replace(NOSCRIPT_URL_REPLACE_PATTERN, getNoscriptUrl(externalContext)); OutputStream os = httpResponse.getOutputStream(); try { os.write(windowHandlerHtml.getBytes()); } finally { os.close(); } } catch (IOException ioe) { throw new FacesException(ioe); } } protected String getNoscriptUrl(ExternalContext externalContext) { String url = externalContext.getRequestPathInfo(); if (url == null) { url = ""; } // only use the very last part of the url int lastSlash = url.lastIndexOf('/'); if (lastSlash != -1) { url = url.substring(lastSlash + 1); } // add request parameter url = JsfUtils.addPageParameters(externalContext, url, true); url = JsfUtils.addParameter(externalContext, url, false, NOSCRIPT_PARAMETER, "true"); // NOTE that the url could contain data for an XSS attack // like e.g. ?"></a><a href%3D"http://hacker.org/attack.html?a // DO NOT REMOVE THE FOLLOWING LINES! url = url.replace("\"", ""); url = url.replace("\'", ""); return url; } protected String getVerifiedWindowIdFromCookie(ExternalContext externalContext) { String cookieName = ClientWindowHelper.Cookies.REQUEST_WINDOW_ID_PREFIX + getRequestTokenParameter(externalContext); Cookie cookie = (Cookie) externalContext.getRequestCookieMap().get(cookieName); if (cookie != null) { // manually blast the cookie away, otherwise it pollutes the // cookie storage in some browsers. E.g. Firefox doesn't // cleanup properly, even if the max-age is reached. cookie.setMaxAge(0); return cookie.getValue(); } return null; } protected String getRequestTokenParameter(ExternalContext externalContext) { String requestToken = externalContext.getRequestParameterMap().get(ClientWindowHelper.RequestParameters.REQUEST_TOKEN); if (requestToken != null) { return requestToken; } return ""; } @Override public String interceptRedirect(FacesContext facesContext, String url) { // following cases we can mark as valid next request: // 1) request == !ajax and GET // A redirect via ExternalContext can only be done in a JSF request. // As the windowId is validated before the JSF lifecycle starts // (via windowhandler streaming/request token validation), we can assume that the current request // is valid and we can just mark the next request/redirect as valid, too. // 2) request == ajax and POST // Ajax is always a "post back", so the browser tab was already validated in earlier requests. // // // following cases we can NOT mark as valid next request: // 1) request == !ajax and POST // This is a Post/Redirect/Get - as the post can be done to a new browser tab // (via the target attribute on the form), the windowId must NOT be valid. // 2) request == ajax and GET // Not a common JSF request. // boolean ajax = facesContext.getPartialViewContext().isAjaxRequest(); boolean post = isPost(facesContext); boolean get = !post; if ((!ajax && get) || (ajax && post)) { String requestToken = generateNewRequestToken(); String windowId = getWindowId(facesContext); ClientWindowHelper.addRequestWindowIdCookie(facesContext, requestToken, windowId); url = JsfUtils.addParameter(facesContext.getExternalContext(), url, true, ClientWindowHelper.RequestParameters.GET_WINDOW_ID, windowId); url = JsfUtils.addParameter(facesContext.getExternalContext(), url, true, ClientWindowHelper.RequestParameters.REQUEST_TOKEN, requestToken); return url; } return url; } }