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}