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