001/**
002 * Copyright 2005-2018 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.web.filter;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.CoreApiServiceLocator;
020import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
021import org.kuali.rice.krad.uif.UifConstants;
022import org.kuali.rice.krad.uif.UifParameters;
023import org.kuali.rice.krad.uif.service.ViewDictionaryService;
024import org.kuali.rice.krad.uif.service.ViewService;
025import org.kuali.rice.krad.uif.view.ViewSessionPolicy;
026import org.kuali.rice.krad.util.KRADConstants;
027import org.kuali.rice.krad.util.KRADUtils;
028import org.kuali.rice.krad.web.form.UifFormManager;
029import org.springframework.web.bind.annotation.RequestMethod;
030
031import javax.servlet.Filter;
032import javax.servlet.FilterChain;
033import javax.servlet.FilterConfig;
034import javax.servlet.ServletException;
035import javax.servlet.ServletRequest;
036import javax.servlet.ServletResponse;
037import javax.servlet.http.HttpServletRequest;
038import javax.servlet.http.HttpServletResponse;
039import javax.servlet.http.HttpSession;
040import java.io.IOException;
041import java.io.PrintWriter;
042import java.util.Map;
043
044/**
045 * Handles session timeouts for KRAD views based on the configured view session policy
046 *
047 * <p>
048 * IMPORTANT! In order to work correctly this filter should be the first filter invoked (even before the login
049 * filter)
050 * </p>
051 *
052 * @author Kuali Rice Team (rice.collab@kuali.org)
053 */
054public class UifSessionTimeoutFilter implements Filter {
055
056    private int sessionTimeoutErrorCode = 403;
057
058    public void init(FilterConfig filterConfig) throws ServletException {
059        String timeoutErrorCode = filterConfig.getInitParameter("sessionTimeoutErrorCode");
060
061        if (timeoutErrorCode != null) {
062            sessionTimeoutErrorCode = Integer.parseInt(timeoutErrorCode);
063        }
064    }
065
066    /**
067     * Checks for a session timeout and if one has occurred pulls the view session policy to determine whether
068     * a redirect needs to happen
069     *
070     * <p>
071     * To determine whether a session timeout has occurred, the filter looks for the existence of a request parameter
072     * named {@link org.kuali.rice.krad.uif.UifParameters#SESSION_ID}. If found it then compares that id to the id
073     * on the current session. If they are different, or a session does not currently exist a timeout is assumed.
074     *
075     * In addition, if a request was made for a form key and the view has session storage enabled, a check is made
076     * to verify the form manager contains a session form. If not this is treated like a session timeout
077     * </p>
078     *
079     * <p>
080     * If a timeout has occurred an attempt is made to resolve a view from the request (based on the view id or
081     * type parameters), then the associated {@link ViewSessionPolicy} is pulled which indicates how the timeout should
082     * be handled. This either results in doing a redirect or nothing
083     * </p>
084     *
085     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse,
086     *      javax.servlet.FilterChain)
087     */
088    public void doFilter(ServletRequest request, ServletResponse response,
089            FilterChain filerChain) throws IOException, ServletException {
090        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
091        HttpSession httpSession = (httpServletRequest).getSession(false);
092
093        boolean timeoutOccurred = false;
094
095        // compare session id in request to id on current session, if different or a session does not exist
096        // then assume a session timeout has occurred
097        if (request.getParameter(UifParameters.SESSION_ID) != null) {
098            String requestedSessionId = request.getParameter(UifParameters.SESSION_ID);
099
100            if ((httpSession == null) || !StringUtils.equals(httpSession.getId(), requestedSessionId)) {
101                timeoutOccurred = true;
102            }
103        }
104
105        String viewId = getViewIdFromRequest(httpServletRequest);
106        if (StringUtils.isBlank(viewId)) {
107            //  can't retrieve a session policy if view id was not passed
108            filerChain.doFilter(request, response);
109
110            return;
111        }
112
113        // check for requested form key for a POST and if found and session storage is enabled for the
114        // view, verify the form is present in the form manager
115        boolean isGetRequest = RequestMethod.GET.name().equals(httpServletRequest.getMethod());
116
117        String formKeyParam = request.getParameter(UifParameters.FORM_KEY);
118        if (StringUtils.isNotBlank(formKeyParam) && !isGetRequest && getViewDictionaryService().isSessionStorageEnabled(
119                viewId) && (httpSession != null)) {
120            UifFormManager uifFormManager = (UifFormManager) httpSession.getAttribute(UifParameters.FORM_MANAGER);
121
122            // if session form not found, treat like a session timeout
123            if ((uifFormManager != null) && !uifFormManager.hasSessionForm(formKeyParam)) {
124                timeoutOccurred = true;
125            }
126        }
127
128        // if no timeout occurred continue filter chain
129        if (!timeoutOccurred) {
130            filerChain.doFilter(request, response);
131
132            return;
133        }
134
135        // retrieve timeout policy associated with the view to determine what steps to take
136        ViewSessionPolicy sessionPolicy = getViewDictionaryService().getViewSessionPolicy(viewId);
137
138        if (sessionPolicy.isRedirectToHome() || StringUtils.isNotBlank(sessionPolicy.getRedirectUrl()) || sessionPolicy
139                .isRenderTimeoutView()) {
140            String redirectUrl = getRedirectUrl(sessionPolicy, httpServletRequest);
141
142            sendRedirect(httpServletRequest, (HttpServletResponse) response, redirectUrl);
143        }
144    }
145
146    /**
147     * Attempts to resolve a view id from the given request
148     *
149     * <p>
150     * First an attempt will be made to find the view id as a request parameter. If no such request parameter
151     * is found, the request will be looked at for view type information and a call will be made to the
152     * view service to find the view id by type
153     * </p>
154     *
155     * <p>
156     * If a view id is found it is stuck in the request as an attribute (under the key
157     * {@link org.kuali.rice.krad.uif.UifParameters#VIEW_ID}) for subsequent retrieval
158     * </p>
159     *
160     * @param request instance to resolve view id for
161     * @return view id if one is found, null if not found
162     */
163    protected String getViewIdFromRequest(HttpServletRequest request) {
164        String viewId = request.getParameter(UifParameters.VIEW_ID);
165
166        if (StringUtils.isBlank(viewId)) {
167            String viewTypeName = request.getParameter(UifParameters.VIEW_TYPE_NAME);
168
169            UifConstants.ViewType viewType = null;
170            if (StringUtils.isNotBlank(viewTypeName)) {
171                viewType = UifConstants.ViewType.valueOf(viewTypeName);
172            }
173
174            if (viewType != null) {
175                @SuppressWarnings("unchecked") Map<String, String> parameterMap =
176                        KRADUtils.translateRequestParameterMap(request.getParameterMap());
177                viewId = getViewService().getViewIdForViewType(viewType, parameterMap);
178            }
179        }
180
181        if (StringUtils.isNotBlank(viewId)) {
182            request.setAttribute(UifParameters.VIEW_ID, viewId);
183        }
184
185        return viewId;
186    }
187
188    /**
189     * Inspects the given view session policy to determine how the request should be redirected
190     *
191     * <p>
192     * The request will either be redirected to the application home, a custom URL, the same request URL but
193     * modified to call the <code>sessionTimeout</code> method, or a redirect to show the session timeout view
194     * </p>
195     *
196     * @param sessionPolicy session policy instance to inspect
197     * @param httpServletRequest request instance for pulling parameters
198     * @return redirect URL or null if no redirect was configured
199     */
200    protected String getRedirectUrl(ViewSessionPolicy sessionPolicy, HttpServletRequest httpServletRequest) {
201        String redirectUrl = null;
202
203        if (sessionPolicy.isRedirectToHome()) {
204            redirectUrl = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
205                    KRADConstants.APPLICATION_URL_KEY);
206        } else if (StringUtils.isNotBlank(sessionPolicy.getRedirectUrl())) {
207            redirectUrl = sessionPolicy.getRedirectUrl();
208        } else if (sessionPolicy.isRenderTimeoutView()) {
209            String kradUrl = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
210                    KRADConstants.KRAD_URL_KEY);
211            redirectUrl = KRADUtils.buildViewUrl(kradUrl, KRADConstants.REQUEST_MAPPING_SESSION_TIMEOUT,
212                    KRADConstants.SESSION_TIMEOUT_VIEW_ID);
213        }
214
215        return redirectUrl;
216    }
217
218    /**
219     * Sends a redirect request either through the standard http redirect mechanism, or by sending back
220     * an Ajax response indicating a redirect should occur
221     *
222     * @param httpServletRequest request instance the timeout occurred for
223     * @param httpServletResponse response object that redirect should occur on
224     * @param redirectUrl url to redirect to
225     * @throws IOException
226     */
227    protected void sendRedirect(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
228            String redirectUrl) throws IOException {
229        // check for an ajax request since the redirects need to happen differently for them
230        boolean ajaxRequest = false;
231
232        String ajaxHeader = httpServletRequest.getHeader("x-requested-with");
233        if ("XMLHttpRequest".equals(ajaxHeader)) {
234            ajaxRequest = true;
235        }
236
237        if (ajaxRequest) {
238            httpServletResponse.setContentType("text/html; charset=UTF-8");
239            httpServletResponse.setCharacterEncoding("UTF-8");
240            httpServletResponse.setStatus(sessionTimeoutErrorCode);
241
242            PrintWriter printWriter = httpServletResponse.getWriter();
243            printWriter.print(redirectUrl);
244
245            printWriter.flush();
246        } else {
247            httpServletResponse.sendRedirect(redirectUrl);
248        }
249    }
250
251    protected static ViewService getViewService() {
252        return KRADServiceLocatorWeb.getViewService();
253    }
254
255    /**
256     * Retrieves implementation of the view dictionary service
257     *
258     * @return view dictionary service instance
259     */
260    protected ViewDictionaryService getViewDictionaryService() {
261        return KRADServiceLocatorWeb.getViewDictionaryService();
262    }
263
264    /**
265     * @see javax.servlet.Filter#destroy()
266     */
267    public void destroy() {
268        // do nothing
269    }
270}