001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
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 *
016 * Contributors:
017 *     arussel
018 */
019package org.nuxeo.ecm.platform.web.common.exceptionhandling;
020
021import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.DISABLE_REDIRECT_REQUEST_KEY;
022import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.FORCE_ANONYMOUS_LOGIN;
023import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGOUT_PAGE;
024import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.REQUESTED_URL;
025import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.SECURITY_ERROR;
026
027import java.io.IOException;
028import java.io.PrintWriter;
029import java.io.StringWriter;
030import java.security.Principal;
031import java.util.HashMap;
032import java.util.Locale;
033import java.util.Map;
034import java.util.ResourceBundle;
035
036import javax.servlet.RequestDispatcher;
037import javax.servlet.ServletException;
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.common.utils.URIUtils;
044import org.nuxeo.common.utils.i18n.I18NUtils;
045import org.nuxeo.ecm.core.api.NuxeoException;
046import org.nuxeo.ecm.core.api.NuxeoPrincipal;
047import org.nuxeo.ecm.core.api.WrappedException;
048import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter;
049import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService;
050import org.nuxeo.ecm.platform.web.common.exceptionhandling.descriptor.ErrorHandler;
051import org.nuxeo.runtime.api.Framework;
052
053/**
054 * @author arussel
055 */
056public class DefaultNuxeoExceptionHandler implements NuxeoExceptionHandler {
057
058    private static final Log log = LogFactory.getLog(DefaultNuxeoExceptionHandler.class);
059
060    protected NuxeoExceptionHandlerParameters parameters;
061
062    @Override
063    public void setParameters(NuxeoExceptionHandlerParameters parameters) {
064        this.parameters = parameters;
065    }
066
067    /**
068     * Puts a marker in request to avoid looping over the exception handling mechanism
069     *
070     * @throws ServletException if request has already been marked as handled. The initial exception is then wrapped.
071     */
072
073    protected void startHandlingException(HttpServletRequest request, HttpServletResponse response, Throwable t)
074            throws ServletException {
075        if (request.getAttribute(EXCEPTION_HANDLER_MARKER) == null) {
076            if (log.isDebugEnabled()) {
077                log.debug("Initial exception", t);
078            }
079            // mark request as already processed by this mechanism to avoid
080            // looping over it
081            request.setAttribute(EXCEPTION_HANDLER_MARKER, true);
082            // disable further redirect by nuxeo url system
083            request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, true);
084        } else {
085            // avoid looping over exception mechanism
086            throw new ServletException(t);
087        }
088    }
089
090    @Override
091    public void handleException(HttpServletRequest request, HttpServletResponse response, Throwable t)
092            throws IOException, ServletException {
093
094        Throwable unwrappedException = ExceptionHelper.unwrapException(t);
095
096        // check for Anonymous case
097        if (ExceptionHelper.isSecurityError(unwrappedException)) {
098            Principal principal = request.getUserPrincipal();
099            if (principal instanceof NuxeoPrincipal) {
100                NuxeoPrincipal nuxeoPrincipal = (NuxeoPrincipal) principal;
101                if (nuxeoPrincipal.isAnonymous()) {
102                    // redirect to login than to requested page
103                    if (handleAnonymousException(request, response)) {
104                        return;
105                    }
106                }
107            }
108        }
109
110        startHandlingException(request, response, t);
111        try {
112            ErrorHandler handler = getHandler(t);
113            Integer code = handler.getCode();
114            int status = code == null ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR : code.intValue();
115            parameters.getListener().startHandling(t, request, response);
116
117            StringWriter swriter = new StringWriter();
118            PrintWriter pwriter = new PrintWriter(swriter);
119            t.printStackTrace(pwriter);
120            String stackTrace = swriter.getBuffer().toString();
121            if (status < HttpServletResponse.SC_INTERNAL_SERVER_ERROR) { // 500
122                log.debug(t.getMessage(), t);
123            } else {
124                log.error(stackTrace);
125                parameters.getLogger().error(stackTrace);
126            }
127
128            parameters.getListener().beforeSetErrorPageAttribute(unwrappedException, request, response);
129            request.setAttribute("exception_message", unwrappedException.getLocalizedMessage());
130            request.setAttribute("user_message", getUserMessage(handler.getMessage(), request.getLocale()));
131            request.setAttribute("securityError", ExceptionHelper.isSecurityError(unwrappedException));
132            request.setAttribute("messageBundle", ResourceBundle.getBundle(parameters.getBundleName(),
133                    request.getLocale(), Thread.currentThread().getContextClassLoader()));
134            String dumpedRequest = parameters.getRequestDumper().getDump(request);
135            if (status >= HttpServletResponse.SC_INTERNAL_SERVER_ERROR) { // 500
136                parameters.getLogger().error(dumpedRequest);
137            }
138            request.setAttribute("isDevModeSet", Framework.isDevModeSet());
139            if (Framework.isDevModeSet()) {
140                request.setAttribute("stackTrace", stackTrace);
141                request.setAttribute("request_dump", dumpedRequest);
142            }
143
144            parameters.getListener().beforeForwardToErrorPage(unwrappedException, request, response);
145            if (!response.isCommitted()) {
146                // The JSP error page needs the response Writer but somebody may already have retrieved
147                // the OutputStream and usage of these two can't be mixed. So we reset the response.
148                response.reset();
149                response.setStatus(status);
150                String errorPage = handler.getPage();
151                errorPage = (errorPage == null) ? parameters.getDefaultErrorPage() : errorPage;
152                RequestDispatcher requestDispatcher = request.getRequestDispatcher(errorPage);
153                if (requestDispatcher != null) {
154                    requestDispatcher.forward(request, response);
155                } else {
156                    log.error("Cannot forward to error page, " + "no RequestDispatcher found for errorPage=" + errorPage
157                            + " handler=" + handler);
158                }
159                parameters.getListener().responseComplete();
160            } else {
161                // do not throw an error, just log it: afterDispatch needs to
162                // be called, and sometimes the initial error is a
163                // ClientAbortException
164                log.error("Cannot forward to error page: " + "response is already committed");
165            }
166            parameters.getListener().afterDispatch(unwrappedException, request, response);
167        } catch (ServletException e) {
168            throw e;
169        } catch (RuntimeException | IOException e) {
170            throw new ServletException(e);
171        }
172    }
173
174    @Override
175    public boolean handleAnonymousException(HttpServletRequest request, HttpServletResponse response)
176            throws IOException, ServletException {
177        PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime()
178                                                                                               .getComponent(
179                                                                                                       PluggableAuthenticationService.NAME);
180        if (authService == null) {
181            return false;
182        }
183        authService.invalidateSession(request);
184        String loginURL = getLoginURL(request);
185        if (loginURL == null) {
186            return false;
187        }
188        if (!response.isCommitted()) {
189            request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, true);
190            response.sendRedirect(loginURL);
191            parameters.getListener().responseComplete();
192        } else {
193            log.error("Cannot redirect to login page: response is already committed");
194        }
195        return true;
196    }
197
198    @Override
199    public String getLoginURL(HttpServletRequest request) {
200        PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime()
201                                                                                               .getComponent(
202                                                                                                       PluggableAuthenticationService.NAME);
203        Map<String, String> urlParameters = new HashMap<>();
204        urlParameters.put(SECURITY_ERROR, "true");
205        urlParameters.put(FORCE_ANONYMOUS_LOGIN, "true");
206        if (request.getAttribute(REQUESTED_URL) != null) {
207            urlParameters.put(REQUESTED_URL, (String) request.getAttribute(REQUESTED_URL));
208        } else {
209            urlParameters.put(REQUESTED_URL, NuxeoAuthenticationFilter.getRequestedUrl(request));
210        }
211        String baseURL = authService.getBaseURL(request) + LOGOUT_PAGE;
212        return URIUtils.addParametersToURIQuery(baseURL, urlParameters);
213    }
214
215    protected ErrorHandler getHandler(Throwable t) {
216        Throwable throwable = ExceptionHelper.unwrapException(t);
217        String className = null;
218        if (throwable instanceof WrappedException) {
219            WrappedException wrappedException = (WrappedException) throwable;
220            className = wrappedException.getClassName();
221        } else {
222            className = throwable.getClass().getName();
223        }
224        for (ErrorHandler handler : parameters.getHandlers()) {
225            if (handler.getError() != null && className.matches(handler.getError())) {
226                return handler;
227            }
228        }
229        throw new NuxeoException("No error handler set.");
230    }
231
232    protected Object getUserMessage(String messageKey, Locale locale) {
233        return I18NUtils.getMessageString(parameters.getBundleName(), messageKey, null, locale);
234    }
235
236}