001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.main;
029
030import org.opencms.crypto.CmsEncryptionException;
031import org.opencms.file.CmsObject;
032import org.opencms.gwt.CmsCoreService;
033import org.opencms.gwt.CmsGwtActionElement;
034import org.opencms.i18n.CmsMessages;
035import org.opencms.security.CmsDefaultAuthorizationHandler;
036import org.opencms.security.CmsRoleViolationException;
037import org.opencms.ui.Messages;
038import org.opencms.ui.login.CmsLoginUI;
039import org.opencms.ui.shared.CmsVaadinConstants;
040import org.opencms.util.CmsRequestUtil;
041import org.opencms.workplace.CmsWorkplace;
042import org.opencms.workplace.CmsWorkplaceLoginHandler;
043import org.opencms.workplace.CmsWorkplaceManager;
044
045import java.io.IOException;
046import java.net.URLEncoder;
047import java.util.Locale;
048import java.util.Map;
049import java.util.concurrent.ConcurrentHashMap;
050
051import javax.servlet.ServletException;
052import javax.servlet.http.HttpServletRequest;
053import javax.servlet.http.HttpServletResponse;
054
055import org.apache.commons.logging.Log;
056
057import org.jsoup.nodes.Document;
058import org.jsoup.nodes.Element;
059import org.jsoup.nodes.Node;
060import org.jsoup.parser.Tag;
061import org.jsoup.select.Elements;
062import org.slf4j.bridge.SLF4JBridgeHandler;
063
064import com.vaadin.server.BootstrapFragmentResponse;
065import com.vaadin.server.BootstrapListener;
066import com.vaadin.server.BootstrapPageResponse;
067import com.vaadin.server.CustomizedSystemMessages;
068import com.vaadin.server.DeploymentConfiguration;
069import com.vaadin.server.RequestHandler;
070import com.vaadin.server.ServiceException;
071import com.vaadin.server.SessionInitEvent;
072import com.vaadin.server.SessionInitListener;
073import com.vaadin.server.SystemMessages;
074import com.vaadin.server.SystemMessagesInfo;
075import com.vaadin.server.SystemMessagesProvider;
076import com.vaadin.server.UIClassSelectionEvent;
077import com.vaadin.server.UIProvider;
078import com.vaadin.server.VaadinRequest;
079import com.vaadin.server.VaadinResponse;
080import com.vaadin.server.VaadinService;
081import com.vaadin.server.VaadinServlet;
082import com.vaadin.server.VaadinServletService;
083import com.vaadin.server.VaadinSession;
084import com.vaadin.shared.ApplicationConstants;
085import com.vaadin.ui.UI;
086
087/**
088 * Servlet for workplace UI requests.<p>
089 */
090public class CmsUIServlet extends VaadinServlet implements SystemMessagesProvider, SessionInitListener {
091
092    /** The bootstrap listener. */
093    static final BootstrapListener BOOTSTRAP_LISTENER = new BootstrapListener() {
094
095        private static final long serialVersionUID = -6249561809984101044L;
096
097        public void modifyBootstrapFragment(BootstrapFragmentResponse response) {
098
099            // nothing to do
100        }
101
102        public void modifyBootstrapPage(BootstrapPageResponse response) {
103
104            CmsCoreService svc = new CmsCoreService();
105            HttpServletRequest request = (HttpServletRequest)VaadinService.getCurrentRequest();
106            svc.setRequest(request);
107            CmsObject cms = ((CmsUIServlet)getCurrent()).getCmsObject();
108            svc.setCms(cms);
109
110            Document doc = response.getDocument();
111            Elements appLoadingElements = doc.getElementsByClass("v-app-loading");
112            if (appLoadingElements.size() > 0) {
113                for (Node node : appLoadingElements.get(0).childNodes()) {
114                    node.remove();
115
116                }
117
118                appLoadingElements.get(0).append(CmsVaadinConstants.LOADING_INDICATOR_HTML);
119            }
120
121            if (shouldShowLogin()) {
122                try {
123                    String html = CmsLoginUI.generateLoginHtmlFragment(cms, response.getRequest());
124                    Element el = new Element(Tag.valueOf("div"), "").html(html);
125                    doc.body().appendChild(el);
126                } catch (IOException e) {
127                    LOG.error(e.getLocalizedMessage(), e);
128                }
129            }
130            Locale currentLocale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
131            // Inject CmsCoreData etc. for GWT dialogs
132            try {
133                doc.head().append(CmsGwtActionElement.exportCommon(cms, svc.prefetch()));
134                doc.head().append(org.opencms.ade.publish.ClientMessages.get().export(currentLocale, true));
135                doc.head().append(org.opencms.ade.upload.ClientMessages.get().export(currentLocale, true));
136                doc.head().append(org.opencms.ade.galleries.ClientMessages.get().export(currentLocale, true));
137                for (String cssURI : OpenCms.getWorkplaceAppManager().getWorkplaceCssUris()) {
138                    doc.head().append("<link rel=\"stylesheet\" href=\"" + CmsWorkplace.getResourceUri(cssURI) + "\">");
139                }
140            } catch (Exception e) {
141                LOG.error(e.getLocalizedMessage(), e);
142            }
143        }
144    };
145
146    /** The static log object for this class. */
147    static final Log LOG = CmsLog.getLog(CmsUIServlet.class);
148
149    /** The login UI provider, overrides the default UI to display the login dialog when required. */
150    static final UIProvider LOGIN_UI_PROVIDER = new UIProvider() {
151
152        private static final long serialVersionUID = 9154828335594149982L;
153
154        @Override
155        public Class<? extends UI> getUIClass(UIClassSelectionEvent event) {
156
157            if (shouldShowLogin() || isLoginUIRequest(event.getRequest())) {
158                return CmsLoginUI.class;
159            }
160            return null;
161        }
162    };
163
164    /** The login redirect handler. */
165    static final RequestHandler REQUEST_AUTHORIZATION_HANDLER = new RequestHandler() {
166
167        private static final long serialVersionUID = 1L;
168
169        public boolean handleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response)
170        throws IOException {
171
172            if (shouldShowLogin() && !isLoginUIRequest(request)) {
173
174                String link = OpenCms.getLinkManager().substituteLinkForUnknownTarget(
175                    ((CmsUIServlet)getCurrent()).getCmsObject(),
176                    CmsWorkplaceLoginHandler.LOGIN_FORM);
177                String requestedUri = ((HttpServletRequest)request).getRequestURI();
178                if (!requestedUri.endsWith(OpenCms.getSystemInfo().getWorkplaceContext())) {
179                    try {
180                        link += "?"
181                            + CmsWorkplaceManager.PARAM_LOGIN_REQUESTED_RESOURCE
182                            + "="
183                            + URLEncoder.encode(requestedUri, "UTF-8")
184                            + "&"
185                            + CmsDefaultAuthorizationHandler.PARAM_ENCRYPTED_REQUESTED_RESOURCE
186                            + "="
187                            + OpenCms.getDefaultTextEncryption().encrypt(requestedUri);
188                    } catch (CmsEncryptionException e) {
189                        LOG.warn(e.getLocalizedMessage(), e);
190                    }
191                }
192                OpenCms.getAuthorizationHandler().requestAuthorization(
193                    (HttpServletRequest)request,
194                    (HttpServletResponse)response,
195                    link);
196                return true;
197            }
198            return false;
199        }
200    };
201
202    /** Serialization id. */
203    private static final long serialVersionUID = 8119684308154724518L;
204
205    // install the slf4j bridge to pipe vaadin logging to log4j
206    static {
207        SLF4JBridgeHandler.install();
208    }
209
210    /** The VAADIN heartbeat request path prefix. */
211    private static final String HEARTBEAT_PREFIX = '/' + ApplicationConstants.HEARTBEAT_PATH + '/';
212
213    /** The current CMS context. */
214    private ThreadLocal<CmsObject> m_perThreadCmsObject = new ThreadLocal<>();
215
216    /** Map of stored system messages objects. */
217    private Map<Locale, SystemMessages> m_systemMessages = new ConcurrentHashMap<Locale, SystemMessages>();
218
219    /** Stores whether the current request is a broadcast poll. */
220    private ThreadLocal<Boolean> m_perThreadBroadcastPoll = new ThreadLocal<>();
221
222    /**
223     * Checks whether the given request was referred from the login page.<p>
224     *
225     * @param request the request
226     *
227     * @return <code>true</code> in case of login ui requests
228     */
229    static boolean isLoginUIRequest(VaadinRequest request) {
230
231        String referrer = request.getHeader("referer");
232        return (referrer != null) && referrer.contains(CmsWorkplaceLoginHandler.LOGIN_HANDLER);
233    }
234
235    /**
236     * Returns whether the login dialog should be shown.<p>
237     *
238     * @return <code>true</code> if the login dialog should be shown
239     */
240    static boolean shouldShowLogin() {
241
242        return ((CmsUIServlet)getCurrent()).getCmsObject().getRequestContext().getCurrentUser().isGuestUser();
243    }
244
245    /**
246     * Returns the current cms context.<p>
247     *
248     * @return the current cms context
249     */
250    public CmsObject getCmsObject() {
251
252        return m_perThreadCmsObject.get();
253    }
254
255    /**
256     * @see com.vaadin.server.SystemMessagesProvider#getSystemMessages(com.vaadin.server.SystemMessagesInfo)
257     */
258    public SystemMessages getSystemMessages(SystemMessagesInfo systemMessagesInfo) {
259
260        Locale locale = systemMessagesInfo.getLocale();
261        if (!m_systemMessages.containsKey(locale)) {
262            m_systemMessages.put(locale, createSystemMessages(locale));
263        }
264        return m_systemMessages.get(locale);
265    }
266
267    /**
268     * @see com.vaadin.server.SessionInitListener#sessionInit(com.vaadin.server.SessionInitEvent)
269     */
270    public void sessionInit(final SessionInitEvent event) {
271
272        // set the locale to the users workplace locale
273        Locale wpLocale = OpenCms.getWorkplaceManager().getWorkplaceLocale(getCmsObject());
274        event.getSession().setLocale(wpLocale);
275        event.getSession().addRequestHandler(REQUEST_AUTHORIZATION_HANDLER);
276        event.getSession().addUIProvider(LOGIN_UI_PROVIDER);
277        event.getSession().addBootstrapListener(BOOTSTRAP_LISTENER);
278    }
279
280    /**
281     * Sets that the current request is a broadcast call.<p>
282     */
283    public void setBroadcastPoll() {
284
285        m_perThreadBroadcastPoll.set(Boolean.TRUE);
286    }
287
288    /**
289     * Sets the current cms context.<p>
290     *
291     * @param cms the current cms context to set
292     */
293    public synchronized void setCms(CmsObject cms) {
294
295        m_perThreadCmsObject.set(cms);
296    }
297
298    /**
299     * @see com.vaadin.server.VaadinServlet#createServletService(com.vaadin.server.DeploymentConfiguration)
300     */
301    @Override
302    protected VaadinServletService createServletService(DeploymentConfiguration deploymentConfiguration)
303    throws ServiceException {
304
305        CmsVaadinServletService service = new CmsVaadinServletService(this, deploymentConfiguration);
306        service.init();
307        return service;
308    }
309
310    /**
311     * @see com.vaadin.server.VaadinServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
312     */
313    @Override
314    protected void service(HttpServletRequest request, HttpServletResponse response)
315    throws ServletException, IOException {
316
317        CmsRequestUtil.disableCrossSiteFrameEmbedding(response);
318        if (request.getRequestURI().contains("/VAADIN")) {
319            super.service(request, response);
320            return;
321        }
322        // check to OpenCms runlevel
323        int runlevel = OpenCmsCore.getInstance().getRunLevel();
324
325        // write OpenCms server identification in the response header
326        response.setHeader(CmsRequestUtil.HEADER_SERVER, OpenCmsCore.getInstance().getSystemInfo().getVersion());
327
328        if (runlevel != OpenCms.RUNLEVEL_4_SERVLET_ACCESS) {
329            // not the "normal" servlet runlevel
330            if (runlevel == OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
331                // we have shell runlevel only, upgrade to servlet runlevel (required after setup wizard)
332                init(getServletConfig());
333            } else {
334                // illegal runlevel, we can't process requests
335                // sending status code 403, indicating the server understood the request but refused to fulfill it
336                response.sendError(HttpServletResponse.SC_FORBIDDEN);
337                // goodbye
338                return;
339            }
340        }
341
342        // check if the given request matches the workplace site
343        if ((OpenCms.getSiteManager().getSites().size() > 1) && !OpenCms.getSiteManager().isWorkplaceRequest(request)) {
344
345            // do not send any redirects to the workplace site for security reasons
346            response.sendError(HttpServletResponse.SC_NOT_FOUND);
347            return;
348        }
349
350        try {
351            OpenCmsCore.getInstance().initCmsContextForUI(request, response, this);
352            super.service(request, response);
353            OpenCms.getSessionManager().updateSessionInfo(getCmsObject(), request, isHeartbeatRequest(request));
354        } catch (CmsRoleViolationException rv) {
355            // don't log these into the error channel
356            LOG.debug(rv.getLocalizedMessage(), rv);
357            // error code not set - set "internal server error" (500)
358            int status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
359            response.setStatus(status);
360            try {
361                response.sendError(status, rv.toString());
362            } catch (IOException e) {
363                // can be ignored
364                LOG.error(e.getLocalizedMessage(), e);
365            }
366        } catch (IOException io) {
367            // probably connection aborted by client, no need to write to the ERROR channel
368            LOG.warn(io.getLocalizedMessage(), io);
369            // try so set status and send error in any case
370            int status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
371            response.setStatus(status);
372            try {
373                response.sendError(status, io.toString());
374            } catch (Exception e) {
375                // can be ignored
376            }
377        } catch (Throwable t) {
378            LOG.error(t.getLocalizedMessage(), t);
379            int status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
380            response.setStatus(status);
381            try {
382                response.sendError(status, t.toString());
383            } catch (IOException e) {
384                // can be ignored
385                LOG.error(e.getLocalizedMessage(), e);
386            }
387        } finally {
388            // remove the thread local cms context
389            clearThreadLocal();
390        }
391    }
392
393    /**
394     * @see com.vaadin.server.VaadinServlet#servletInitialized()
395     */
396    @Override
397    protected void servletInitialized() throws ServletException {
398
399        super.servletInitialized();
400        getService().setSystemMessagesProvider(this);
401        getService().addSessionInitListener(this);
402    }
403
404    /**
405     * Clears the thread local storage.<p>
406     */
407    private void clearThreadLocal() {
408
409        m_perThreadCmsObject.set(null);
410        m_perThreadBroadcastPoll.remove();
411    }
412
413    /**
414     * Returns a system messages instance for the given locale.<p>
415     *
416     * @param locale the locale
417     *
418     * @return the system messages
419     */
420    private SystemMessages createSystemMessages(Locale locale) {
421
422        CmsMessages messages = Messages.get().getBundle(locale);
423        CustomizedSystemMessages systemMessages = new CustomizedSystemMessages();
424        systemMessages.setCommunicationErrorCaption(messages.key(Messages.GUI_SYSTEM_COMMUNICATION_ERROR_CAPTION_0));
425        systemMessages.setCommunicationErrorMessage(messages.key(Messages.GUI_SYSTEM_COMMUNICATION_ERROR_MESSAGE_0));
426        systemMessages.setCommunicationErrorNotificationEnabled(true);
427        systemMessages.setAuthenticationErrorCaption(messages.key(Messages.GUI_SYSTEM_AUTHENTICATION_ERROR_CAPTION_0));
428        systemMessages.setAuthenticationErrorMessage(messages.key(Messages.GUI_SYSTEM_AUTHENTICATION_ERROR_MESSAGE_0));
429        systemMessages.setAuthenticationErrorNotificationEnabled(true);
430        systemMessages.setSessionExpiredCaption(messages.key(Messages.GUI_SYSTEM_SESSION_EXPIRED_ERROR_CAPTION_0));
431        systemMessages.setSessionExpiredMessage(messages.key(Messages.GUI_SYSTEM_SESSION_EXPIRED_ERROR_MESSAGE_0));
432        systemMessages.setSessionExpiredNotificationEnabled(true);
433        systemMessages.setInternalErrorCaption(messages.key(Messages.GUI_SYSTEM_INTERNAL_ERROR_CAPTION_0));
434        systemMessages.setInternalErrorMessage(messages.key(Messages.GUI_SYSTEM_INTERNAL_ERROR_MESSAGE_0));
435        systemMessages.setInternalErrorNotificationEnabled(true);
436        return systemMessages;
437    }
438
439    /**
440     * Checks whether the given request is a heartbeat request.<p>
441     *
442     * @param request the request
443     *
444     * @return <code>true</code> in case of VAADIN heartbeat requests
445     */
446    private boolean isHeartbeatRequest(HttpServletRequest request) {
447
448        if ((m_perThreadBroadcastPoll.get() != null) && m_perThreadBroadcastPoll.get().booleanValue()) {
449            return true;
450        }
451        String pathInfo = request.getPathInfo();
452        return (pathInfo != null) && pathInfo.startsWith(HEARTBEAT_PREFIX);
453    }
454}