/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
*/
package org.olat.dispatcher;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.olat.admin.user.delete.service.UserDeletionManager;
import org.olat.basesecurity.AuthHelper;
import org.olat.core.CoreSpringFactory;
import org.olat.core.dispatcher.Dispatcher;
import org.olat.core.dispatcher.DispatcherModule;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.UserRequestImpl;
import org.olat.core.gui.Windows;
import org.olat.core.gui.components.Window;
import org.olat.core.gui.control.ChiefController;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.gui.render.URLBuilder;
import org.olat.core.id.Identity;
import org.olat.core.id.context.BusinessControl;
import org.olat.core.id.context.BusinessControlFactory;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.ThreadLocalUserActivityLoggerInstaller;
import org.olat.core.util.StringHelper;
import org.olat.core.util.UserSession;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.i18n.I18nManager;
import org.olat.core.util.i18n.I18nModule;
import org.olat.core.util.session.UserSessionManager;
import org.olat.login.LoginModule;
import org.olat.restapi.security.RestSecurityBean;
import org.olat.restapi.security.RestSecurityHelper;
/**
* Description:<br>
* Entry point for Resource URL's which are a replacement for the jumpIn / Go Repo style URL's. The assumption is, that the URL here
* set up from a list of BusinessControls containing a (type/resource)name and an (type/resource)id of type long.</br>
* e.g. [RepoyEntry:12323123][CourseNode:2341231456][message:123123][blablup:555555] which is mapped to</br>
* /RepoyEntry/12323123/CourseNode/2341231456/message/123123/blablup/555555/</p>
* This dispatcher does the reverse mapping and creation of a list of BusinessControls which can be used to activate/spawn the Controller.
* The same mechanism is used for lucene search engine and the activation of search results.
* <p>
* This dispatcher supports also a simple single sign-on-mechanism (SS). If an URL contains the parameter X-OLAT-TOKEN, the
* RestSecurityBean will be used to look up the associated user. You can use the REST API to create such a X-OLAT-TOKEN or
* replace the RestSecurityBean with your own implementation that creates the tokens. Please refere to the REST API documentation
* on how to create the X-OLAT-TOKEN
* <br />
* Example: [RepoyEntry:12323123][CourseNode:2341231456][message:123123][blablup:555555]?X-OLAT-TOKEN=xyz
* <P>
* TODO:pb:2009-06-02: (1) Check for Authenticated Session, otherwise send over login page (2) UZHDisparcher has a security check for
* use of SSL -> introduce also here or maybe bring the check into webapphelper.
* <P>
* Initial Date: 24.04.2009 <br>
* @author patrickb
*/
public class RESTDispatcher implements Dispatcher {
private static final OLog log = Tracing.createLoggerFor(RESTDispatcher.class);
@Override
public void execute(HttpServletRequest request, HttpServletResponse response) {
//
// create a ContextEntries String which can be used to create a BusinessControl -> move to
//
String uriPrefix = DispatcherModule.getLegacyUriPrefix(request);
final String origUri = request.getRequestURI();
String restPart = origUri.substring(uriPrefix.length());
try {
restPart = URLDecoder.decode(restPart, "UTF8");
} catch (UnsupportedEncodingException e) {
log.error("Unsupported encoding", e);
}
String[] split = restPart.split("/");
if (split.length % 2 != 0) {
// assert(split.length % 2 == 0);
//The URL is not a valid business path
DispatcherModule.sendBadRequest(origUri, response);
log.warn("URL is not valid: "+restPart);
return;
}
String businessPath = BusinessControlFactory.getInstance().formatFromSplittedURI(split);
if(log.isDebug()) {
log.debug("REQUEST URI: " + origUri);
log.debug("REQUEST PREFIX " + restPart);
log.debug("calc buspath " + businessPath);
}
//check if the businesspath is valid
try {
BusinessControl bc = BusinessControlFactory.getInstance().createFromString(businessPath);
if(!bc.hasContextEntry()) {
//The URL is not a valid business path
DispatcherModule.sendBadRequest(origUri, response);
return;
}
} catch (Exception e) {
DispatcherModule.sendBadRequest(origUri, response);
log.warn("Error with business path: " + origUri, e);
return;
}
//
// create the olat ureq and get an associated main window to spawn the "tab"
//
UserSession usess = CoreSpringFactory.getImpl(UserSessionManager.class).getUserSession(request);
if(usess != null) {
ThreadLocalUserActivityLoggerInstaller.initUserActivityLogger(request);
}
UserRequest ureq = null;
try {
//upon creation URL is checked for
ureq = new UserRequestImpl(uriPrefix, request, response);
} catch(NumberFormatException nfe) {
//MODE could not be decoded
//typically if robots with wrong urls hit the system
//or user have bookmarks
//or authors copy-pasted links to the content.
//showing redscreens for non valid URL is wrong instead
//a 404 message must be shown -> e.g. robots correct their links.
if(log.isDebug()){
log.debug("Bad Request "+request.getPathInfo());
}
DispatcherModule.sendBadRequest(request.getPathInfo(), response);
return;
}
//XX:GUIInterna.setLoadPerformanceMode(ureq);
// Do auto-authenticate if url contains a X-OLAT-TOKEN Single-Sign-On REST-Token
String xOlatToken = ureq.getParameter(RestSecurityHelper.SEC_TOKEN);
if (xOlatToken != null) {
// Lookup identity that is associated with this token
RestSecurityBean securityBean = (RestSecurityBean)CoreSpringFactory.getBean(RestSecurityBean.class);
Identity restIdentity = securityBean.getIdentity(xOlatToken);
//
if(log.isDebug()) {
if (restIdentity == null)
log.debug("Found SSO token " + RestSecurityHelper.SEC_TOKEN + " in url, but token is not bound to an identity");
else
log.debug("Found SSO token " + RestSecurityHelper.SEC_TOKEN + " in url which is bound to identity::" + restIdentity.getName());
}
//
if (restIdentity != null) {
// Test if the current OLAT session does already belong to this user.
// The session could be an old session from another user or it could
// belong to this user but miss the window object because so far it was
// a head-less REST session. REST sessions initially have a small
// timeout, however OLAT does set the standard session timeout on each
// UserSession.getSession() request. This means, the normal session
// timeout is set in the redirect request that will happen immediately
// after the REST dispatcher finishes. No need to change it here.
if (!usess.isAuthenticated() || !restIdentity.equalsByPersistableKey(usess.getIdentity())) {
// Re-authenticate user session for this user and start a fresh
// standard OLAT session
int loginStatus = AuthHelper.doLogin(restIdentity, RestSecurityHelper.SEC_TOKEN, ureq);
if (loginStatus == AuthHelper.LOGIN_OK) {
//fxdiff: FXOLAT-268 update last login date and register active user
UserDeletionManager.getInstance().setIdentityAsActiv(restIdentity);
} else {
//error, redirect to login screen
DispatcherModule.redirectToDefaultDispatcher(response);
}
} else if (Windows.getWindows(usess).getChiefController() == null) {
// Session is already available, but no main window (Head-less REST
// session). Only create the base chief controller and the window
Window currentWindow = AuthHelper.createAuthHome(ureq).getWindow();
//the user is authenticated successfully with a security token, we can set the authenticated path
currentWindow.setUriPrefix(WebappHelper.getServletContextPath() + DispatcherModule.PATH_AUTHENTICATED);
Windows ws = Windows.getWindows(ureq);
ws.registerWindow(currentWindow);
// no need to call setIdentityAsActive as this was already done by RestApiLoginFilter...
}
}
}
boolean auth = usess.isAuthenticated();
if (auth) {
if (Windows.getWindows(usess).getChiefController() == null) {
// Session is already available, but no main window (Head-less REST
// session). Only create the base chief controller and the window
setBusinessPathInUserSession(usess, businessPath, ureq.getParameter(WINDOW_SETTINGS));
AuthHelper.createAuthHome(ureq);
String url = getRedirectToURL(usess) + ";jsessionid=" + usess.getSessionInfo().getSession().getId();
DispatcherModule.redirectTo(response, url);
} else {
//redirect to the authenticated dispatcher which support REST url
String url = WebappHelper.getServletContextPath() + DispatcherModule.PATH_AUTHENTICATED + restPart;
DispatcherModule.redirectTo(response, url);
}
} else {
//prepare for redirect
LoginModule loginModule = CoreSpringFactory.getImpl(LoginModule.class);
setBusinessPathInUserSession(usess, businessPath, ureq.getParameter(WINDOW_SETTINGS));
String invitationAccess = ureq.getParameter(AuthenticatedDispatcher.INVITATION);
if (invitationAccess != null && loginModule.isInvitationEnabled()) {
// try to log in as anonymous
// use the language from the lang paramter if available, otherwhise use the system default locale
Locale guestLoc = getLang(ureq);
int loginStatus = AuthHelper.doInvitationLogin(invitationAccess, ureq, guestLoc);
if ( loginStatus == AuthHelper.LOGIN_OK) {
Identity invite = usess.getIdentity();
//fxdiff: FXOLAT-268 update last login date and register active user
UserDeletionManager.getInstance().setIdentityAsActiv(invite);
//logged in as invited user, continue
String url = getRedirectToURL(usess);
DispatcherModule.redirectTo(response, url);
} else if (loginStatus == AuthHelper.LOGIN_NOTAVAILABLE) {
DispatcherModule.redirectToServiceNotAvailable(response);
} else {
//error, redirect to login screen
DispatcherModule.redirectToDefaultDispatcher(response);
}
} else {
String guestAccess = ureq.getParameter(AuthenticatedDispatcher.GUEST);
if (guestAccess == null || !loginModule.isGuestLoginLinksEnabled()) {
DispatcherModule.redirectToDefaultDispatcher(response);
return;
} else if (guestAccess.equals(AuthenticatedDispatcher.TRUE)) {
// try to log in as anonymous
// use the language from the lang paramter if available, otherwhise use the system default locale
Locale guestLoc = getLang(ureq);
int loginStatus = AuthHelper.doAnonymousLogin(ureq, guestLoc);
if ( loginStatus == AuthHelper.LOGIN_OK) {
//logged in as anonymous user, continue
String url = getRedirectToURL(usess);
DispatcherModule.redirectTo(response, url);
} else if (loginStatus == AuthHelper.LOGIN_NOTAVAILABLE) {
DispatcherModule.redirectToServiceNotAvailable(response);
} else {
//error, redirect to login screen
DispatcherModule.redirectToDefaultDispatcher(response);
}
}
}
}
}
/**
* The method allows for a finite sets of business path to redirect to the DMZ
* @param usess
* @param businessPath
*/
//fxdiff FXOLAT-113: business path in DMZ
private void setBusinessPathInUserSession(UserSession usess, String businessPath, String options) {
if(StringHelper.containsNonWhitespace(businessPath) && usess != null) {
if(businessPath.startsWith("[changepw:0]") || "[registration:0]".equals(businessPath) || "[guest:0]".equals(businessPath)
|| "[browsercheck:0]".equals(businessPath) || "[accessibility:0]".equals(businessPath) || "[about:0]".equals(businessPath)) {
usess.putEntryInNonClearedStore(DMZDispatcher.DMZDISPATCHER_BUSINESSPATH, businessPath);
} else {
usess.putEntryInNonClearedStore(AuthenticatedDispatcher.AUTHDISPATCHER_BUSINESSPATH, businessPath);
}
}
if(options != null && usess != null) {
usess.putEntryInNonClearedStore(WINDOW_SETTINGS, options);
}
}
private Locale getLang(UserRequest ureq) {
// try to log in as anonymous
// use the language from the lang parameter if available, otherwise use the system default locale
String guestLang = ureq.getParameter("language");
if (guestLang == null) {
// support for legacy lang parameter
guestLang = ureq.getParameter("lang");
}
Locale guestLoc;
if (guestLang == null) {
guestLoc = I18nModule.getDefaultLocale();
} else {
guestLoc = I18nManager.getInstance().getLocaleOrDefault(guestLang);
}
return guestLoc;
}
private String getRedirectToURL(UserSession usess) {
ChiefController cc = Windows.getWindows(usess).getChiefController();
Window w = cc.getWindow();
URLBuilder ubu = new URLBuilder(WebappHelper.getServletContextPath() + DispatcherModule.PATH_AUTHENTICATED, w.getInstanceId(), String.valueOf(w.getTimestamp()));
StringOutput sout = new StringOutput(30);
ubu.buildURI(sout, null, null);
return sout.toString();
}
}