/** * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-portlet/src/java/org/sakaiproject/blti/ProviderServlet.java $ * $Id: ProviderServlet.java 131400 2013-11-11 15:57:48Z a.fish@lancaster.ac.uk $ * * Copyright (c) 2009 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sakaiproject.blti; import java.lang.reflect.Method; import java.io.IOException; import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.*; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.oauth.*; import net.oauth.server.OAuthServlet; import net.oauth.signature.OAuthSignatureMethod; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.imsglobal.basiclti.BasicLTIConstants; import org.imsglobal.basiclti.BasicLTIUtil; import org.sakaiproject.authz.api.Member; import org.sakaiproject.authz.api.Role; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.cover.SecurityService; import org.sakaiproject.lti.api.BLTIProcessor; import org.sakaiproject.lti.api.LTIException; import org.sakaiproject.basiclti.util.ShaUtil; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.entity.api.ResourcePropertiesEdit; import org.sakaiproject.event.api.NotificationService; import org.sakaiproject.event.cover.UsageSessionService; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.id.cover.IdManager; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SitePage; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.Tool; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.tool.cover.ToolManager; import org.sakaiproject.user.api.Preferences; import org.sakaiproject.user.api.PreferencesEdit; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserNotDefinedException; import org.sakaiproject.user.cover.PreferencesService; import org.sakaiproject.user.cover.UserDirectoryService; import org.sakaiproject.util.ResourceLoader; import org.springframework.context.ApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; /** * Notes: * * This program is directly exposed as a URL to receive IMS Basic LTI launches * so it must be carefully reviewed and any changes must be looked at carefully. * Here are some issues: * * - This will only function when it is enabled via sakai.properties * * - This servlet makes use of security advisors - once an advisor has been * added, it must be removed - often in a finally. Also the code below only adds * the advisor for very short segments of code to allow for easier review. * * Implemented using a SHA-1 hash of the effective context_id and then stores * the original context_id in a site.property "lti_context_id" which will be * useful for later reference. Since SHA-1 hashes to 40 chars, that would leave * us 59 chars (i.e. 58 + ":") to use for LTI key. This also means that the new * maximum supported size of an effective context_id is the maximum message size * of SHA-1: maximum length of (264 ? 1) bits. */ @SuppressWarnings("deprecation") public class ProviderServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static Log M_log = LogFactory.getLog(ProviderServlet.class); private static ResourceLoader rb = new ResourceLoader("basiclti"); private static final String BASICLTI_RESOURCE_LINK = "blti:resource_link_id"; private static final String LTI_CONTEXT_ID = "lti_context_id"; private Object profileImageLogicObject = null; private Method saveOfficialImageUrlMethod = null; private Object profilePreferencesLogicObject = null; private Method setUseOfficialImageMethod = null; private Method getPreferencesRecordForUserMethod = null; private Method savePreferencesRecordMethod = null; private List<BLTIProcessor> bltiProcessors = new ArrayList(); private enum ProcessingState { beforeValidation, afterValidation, afterUserCreation, afterLogin, afterSiteCreation, afterSiteMembership, beforeLaunch } /** * Setup a security advisor. */ public void pushAdvisor() { // setup a security advisor SecurityService.pushAdvisor(new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { return SecurityAdvice.ALLOWED; } }); } /** * Remove our security advisor. */ public void popAdvisor() { SecurityService.popAdvisor(); } public void doError(HttpServletRequest request,HttpServletResponse response, String s, String message, Throwable e) throws java.io.IOException { if (e != null) { M_log.error(e.getLocalizedMessage(), e); } M_log.info(rb.getString(s) + ": " + message); String return_url = request.getParameter(BasicLTIConstants.LAUNCH_PRESENTATION_RETURN_URL); if (return_url != null && return_url.length() > 1) { if (return_url.indexOf('?') > 1) { return_url += "<i_msg=" + URLEncoder.encode(rb.getString(s), "UTF-8"); } else { return_url += "?lti_msg=" + URLEncoder.encode(rb.getString(s), "UTF-8"); } // Avoid Response Splitting return_url = return_url.replaceAll("[\r\n]",""); response.sendRedirect(return_url); return; } PrintWriter out = response.getWriter(); out.println(rb.getString(s)); } @Override public void init(ServletConfig config) throws ServletException { super.init(config); ApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(config.getServletContext()); // load all instance of BLTIProcessor in component mgr by type detection Collection processors = ac.getParent().getBeansOfType(BLTIProcessor.class).values(); bltiProcessors = new ArrayList(processors); // sort in using getOrder() method // sort them so the execution order is determined consistenly - by getOrder() Collections.sort(bltiProcessors, new Comparator() { public int compare(Object o1, Object o2) { return ((Comparable) ((BLTIProcessor) (o1)).getOrder()) .compareTo(((BLTIProcessor) (o2)).getOrder()); } }); setupProfile2Methods(); } /** * BLTI-155. Use reflection to lookup the Profile2 methods we need for setting profile * pictures provided by consumers */ private void setupProfile2Methods() { // BLTI-155 START // Test whether Profile2 is available and setup the reflective methods if so profileImageLogicObject = ComponentManager.getInstance().get("org.sakaiproject.profile2.logic.ProfileImageLogic"); profilePreferencesLogicObject = ComponentManager.getInstance().get("org.sakaiproject.profile2.logic.ProfilePreferencesLogic"); if(profileImageLogicObject != null && profilePreferencesLogicObject != null) { M_log.debug("Profile2 is installed."); // It is. Cache the methods for later use. try { saveOfficialImageUrlMethod = profileImageLogicObject.getClass().getMethod("saveOfficialImageUrl", new Class[] { String.class,String.class}); getPreferencesRecordForUserMethod = profilePreferencesLogicObject.getClass().getMethod("getPreferencesRecordForUser", new Class[] { String.class }); Class preferencesClazz = Class.forName("org.sakaiproject.profile2.model.ProfilePreferences"); setUseOfficialImageMethod = preferencesClazz.getMethod("setUseOfficialImage", new Class[] { boolean.class}); savePreferencesRecordMethod = profilePreferencesLogicObject.getClass().getMethod("savePreferencesRecord", new Class[] { preferencesClazz }); M_log.debug("Methods cached."); } catch(Exception e) { M_log.warn("Tried to locate the profile2 api but failed. Consumer user_image launch parameters WILL NOT be shown"); } } // BLTI-155 END } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } protected Map getPayloadAsMap(HttpServletRequest request) { Map payload = new HashMap(); for (Enumeration e = request.getParameterNames(); e.hasMoreElements(); ) { String key = (String)e.nextElement(); payload.put(key, request.getParameter(key)); } payload.put("oauth_message", OAuthServlet.getMessage(request, null)); payload.put("tool_id", request.getPathInfo()); return payload; } @SuppressWarnings("unchecked") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String ipAddress = request.getRemoteAddr(); if (M_log.isDebugEnabled()) { M_log.debug("Basic LTI Provider request from IP=" + ipAddress); } String enabled = ServerConfigurationService.getString( "basiclti.provider.enabled", null); if (enabled == null || !("true".equals(enabled))) { M_log.warn("Basic LTI Provider is Disabled IP=" + ipAddress); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Basic LTI Provider is Disabled"); return; } if (M_log.isDebugEnabled()) { Map<String, String[]> params = (Map<String, String[]>) request .getParameterMap(); for (Map.Entry<String, String[]> param : params.entrySet()) { M_log.debug(param.getKey() + ":" + param.getValue()[0]); } } Map payload = getPayloadAsMap(request); // Get the list of highly trusted consumers from sakai.properties. // If the incoming consumer is highly trusted, we use the context_id and site_id as is, // ie without prefixing them with the oauth_consumer_key first. // We also don't both checking their roles in the site. boolean isTrustedConsumer = isTrustedConsumer(payload); try { invokeProcessors(payload, isTrustedConsumer, ProcessingState.beforeValidation); validate(payload, isTrustedConsumer); invokeProcessors(payload, isTrustedConsumer, ProcessingState.afterValidation); User user = findOrCreateUser(payload, isTrustedConsumer); invokeProcessors(payload, isTrustedConsumer, ProcessingState.afterUserCreation, user); loginUser(ipAddress, user); // This needs to happen after login, when we have a session for the user. setupUserLocale(payload, user, isTrustedConsumer); setupUserPicture(payload, user, isTrustedConsumer); invokeProcessors(payload, isTrustedConsumer, ProcessingState.afterLogin, user); Site site = findOrCreateSite(payload, isTrustedConsumer); setupUserEmailPreferenceForSite(payload, user, site, isTrustedConsumer); invokeProcessors(payload, isTrustedConsumer, ProcessingState.afterSiteCreation, user, site); site = addOrUpdateSiteMembership(payload, isTrustedConsumer, user, site); invokeProcessors(payload, isTrustedConsumer, ProcessingState.afterSiteMembership, user, site); String toolPlacementId = addOrCreateTool(payload, isTrustedConsumer, user, site); invokeProcessors(payload, isTrustedConsumer, ProcessingState.beforeLaunch, user, site); // Construct a URL to this tool StringBuilder url = new StringBuilder(); url.append(ServerConfigurationService.getServerUrl()); url.append(ServerConfigurationService.getString("portalPath", "/portal")); url.append("/tool-reset/"); url.append(toolPlacementId); url.append("?panel=Main"); if (M_log.isDebugEnabled()) { M_log.debug("url=" + url.toString()); } //String toolLink = ServerConfigurationService.getPortalUrl()+ "/tool-reset/" + placement_id + "?panel=Main"; // Compensate for bug in getPortalUrl() //toolLink = toolLink.replace("IMS BLTI Portlet", "portal"); response.setContentType("text/html"); response.setStatus(HttpServletResponse.SC_FOUND); response.sendRedirect(url.toString()); } catch (LTIException ltiException) { doError(request, response, ltiException.getErrorKey(), ltiException.getMessage(), ltiException.getCause()); } /* PrintWriter out = response.getWriter(); out.println("<body><div style=\"text-align: center\">"); out.println(" <br/> <br/> <br/> <br/>"); out.println(" <br/> <br/> <br/> <br/>"); out.println("<a href=\"" + url.toString() + "\">"); out.println("<span id=\"hideme\">" + rb.getString("launch.continue") + "</span>"); out.println("</a>"); out.println(" <script language=\"javascript\"> \n" + " document.getElementById(\"hideme\").style.display = \"none\";\n" + " location.href=\"" + url.toString() + "\";\n" + " </script> \n"); out.println("</div>"); out.println("</body>"); out.close(); */ } protected void invokeProcessors(Map payload, boolean trustedConsumer, ProcessingState processingState, User user) throws LTIException { invokeProcessors(payload, trustedConsumer, processingState, user, null, null); } protected void invokeProcessors(Map payload, boolean trustedConsumer, ProcessingState processingState) throws LTIException{ invokeProcessors(payload, trustedConsumer, processingState, null, null, null); } protected void invokeProcessors(Map payload, boolean trustedConsumer, ProcessingState processingState, User user, Site site) throws LTIException{ invokeProcessors(payload, trustedConsumer, processingState, user, site, null); } protected void invokeProcessors(Map payload, boolean trustedConsumer, ProcessingState processingState, User user, Site site, String toolPlacementId) throws LTIException{ if (!bltiProcessors.isEmpty()) { for (BLTIProcessor processor : bltiProcessors) { switch (processingState) { case beforeValidation: processor.beforeValidation(payload, trustedConsumer); break; case afterValidation: processor.afterValidation(payload, trustedConsumer); break; case afterUserCreation: processor.afterUserCreation(payload, user); break; case afterLogin: processor.afterLogin(payload, trustedConsumer, user); break; case afterSiteCreation: processor.afterSiteCreation(payload, trustedConsumer, user, site); break; case afterSiteMembership: processor.afterSiteMembership(payload, trustedConsumer, user, site); break; case beforeLaunch: processor.beforeLaunch(payload, trustedConsumer, user, site, toolPlacementId); break; default: M_log.error("unknown processing state of " + processingState); } } } } protected void validate(Map payload, boolean isTrustedConsumer) throws LTIException { //check parameters String lti_message_type = (String) payload.get(BasicLTIConstants.LTI_MESSAGE_TYPE); String lti_version = (String) payload.get(BasicLTIConstants.LTI_VERSION); String oauth_consumer_key = (String) payload.get("oauth_consumer_key"); String resource_link_id = (String) payload.get(BasicLTIConstants.RESOURCE_LINK_ID); String user_id = (String) payload.get(BasicLTIConstants.USER_ID); String context_id = (String) payload.get(BasicLTIConstants.CONTEXT_ID); if(!BasicLTIUtil.equals(lti_message_type, "basic-lti-launch-request")) { throw new LTIException("launch.invalid", "lti_message_type="+lti_message_type, null); } if(!BasicLTIUtil.equals(lti_version, "LTI-1p0")) { throw new LTIException( "launch.invalid", "lti_version="+lti_version, null); } if(BasicLTIUtil.isBlank(oauth_consumer_key)) { throw new LTIException( "launch.missing", "oauth_consumer_key", null); } if(BasicLTIUtil.isBlank(resource_link_id)) { throw new LTIException( "launch.missing", "resource_link_id", null); } if(BasicLTIUtil.isBlank(user_id)) { throw new LTIException( "launch.missing", "user_id", null); } if (M_log.isDebugEnabled()) { M_log.debug("user_id=" + user_id); } //check tool_id String tool_id = (String) payload.get("tool_id"); if (tool_id == null) { throw new LTIException("launch.tool_id.required", null, null); } // Trim off the leading slash and any trailing space tool_id = tool_id.substring(1).trim(); if (M_log.isDebugEnabled()) { M_log.debug("tool_id=" + tool_id); } // store modified tool_id back in payload payload.put("tool_id", tool_id); final String allowedToolsConfig = ServerConfigurationService.getString("basiclti.provider.allowedtools", ""); final String[] allowedTools = allowedToolsConfig.split(":"); final List<String> allowedToolsList = Arrays.asList(allowedTools); if (allowedTools != null && !allowedToolsList.contains(tool_id)) { throw new LTIException( "launch.tool.notallowed", tool_id, null); } final Tool toolCheck = ToolManager.getTool(tool_id); if (toolCheck == null) { throw new LTIException("launch.tool.notfound", tool_id, null); } // Check for the ext_sakai_provider_eid param. If set, this will contain the eid that we are to use // in place of using the user_id parameter // WE still need that parameter though, so translate it from the given eid. boolean useProvidedEid = false; String ext_sakai_provider_eid = (String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID); if(BasicLTIUtil.isNotBlank(ext_sakai_provider_eid)){ useProvidedEid = true; try { user_id = UserDirectoryService.getUserId(ext_sakai_provider_eid); } catch (Exception e) { M_log.error(e.getLocalizedMessage(), e); throw new LTIException("launch.provided.eid.invalid", "ext_sakai_provider_eid="+ext_sakai_provider_eid, e); } } if (M_log.isDebugEnabled()) { M_log.debug("ext_sakai_provider_eid=" + ext_sakai_provider_eid); } // Contextualize the context_id with the OAuth consumer key // Also use the resource_link_id for the context_id if we did not get a context_id // BLTI-31: if trusted, context_id is required and use the param without modification if(BasicLTIUtil.isBlank(context_id)) { if(isTrustedConsumer) { throw new LTIException( "launch.missing",context_id, null); } else { context_id = "res:" + resource_link_id; payload.put(BasicLTIConstants.CONTEXT_ID, context_id); } } // Check if context_id is simply a ~. If so, get the id of that user's My Workspace site // and use that to construct the full context_id if(BasicLTIUtil.equals(context_id, "~")){ if(useProvidedEid) { String userSiteId = null; try { userSiteId = SiteService.getUserSiteId(user_id); } catch (Exception e) { M_log.warn("Failed to get My Workspace site for user_id:" + user_id); M_log.error(e.getLocalizedMessage(), e); throw new LTIException( "launch.user.site.unknown", "user_id="+user_id, e); } context_id = userSiteId; payload.put(BasicLTIConstants.CONTEXT_ID, context_id); } } if (M_log.isDebugEnabled()) { M_log.debug("context_id=" + context_id); } // Lookup the secret final String configPrefix = "basiclti.provider." + oauth_consumer_key + "."; final String oauth_secret = ServerConfigurationService.getString(configPrefix+ "secret", null); if (oauth_secret == null) { throw new LTIException( "launch.key.notfound",oauth_consumer_key, null); } final OAuthMessage oam = (OAuthMessage) payload.get("oauth_message"); final String forcedURIScheme = ServerConfigurationService.getString("basiclti.provider.forcedurischeme", null); if(forcedURIScheme != null) { try { URI testURI = new URI(oam.URL); URI newURI = new URI(forcedURIScheme,testURI.getSchemeSpecificPart(),null); oam.URL = newURI.toString(); } catch (URISyntaxException use) { } } final OAuthValidator oav = new SimpleOAuthValidator(); final OAuthConsumer cons = new OAuthConsumer("about:blank#OAuth+CallBack+NotUsed", oauth_consumer_key,oauth_secret, null); final OAuthAccessor acc = new OAuthAccessor(cons); String base_string = null; try { base_string = OAuthSignatureMethod.getBaseString(oam); } catch (Exception e) { M_log.error(e.getLocalizedMessage(), e); base_string = null; } try { oav.validateMessage(oam, acc); } catch (Exception e) { M_log.warn("Provider failed to validate message"); M_log.warn(e.getLocalizedMessage(), e); if (base_string != null) { M_log.warn(base_string); } throw new LTIException( "launch.no.validate", context_id, e); } final Session sess = SessionManager.getCurrentSession(); if (sess == null) { throw new LTIException( "launch.no.session", context_id, null); } } private String addOrCreateTool(Map payload, boolean trustedConsumer, User user, Site site) throws LTIException { // Check if the site already has the tool String toolPlacementId = null; String tool_id = (String) payload.get("tool_id"); try { site = SiteService.getSite(site.getId()); ToolConfiguration toolConfig = site.getToolForCommonId(tool_id); if(toolConfig != null) { toolPlacementId = toolConfig.getId(); } } catch (Exception e) { M_log.warn(e.getLocalizedMessage(), e); new LTIException( "launch.tool.search", "tool_id="+tool_id, e); } if (M_log.isDebugEnabled()) { M_log.debug("toolPlacementId=" + toolPlacementId); } // If tool not in site, and we are a trusted consumer, error // Otherwise, add tool to the site ToolConfiguration toolConfig = null; if(BasicLTIUtil.isBlank(toolPlacementId)) { if(trustedConsumer) { new LTIException("launch.site.tool.missing", "tool_id="+tool_id + ", siteId="+site.getId(), null); } else { try { SitePage sitePageEdit = null; sitePageEdit = site.addPage(); sitePageEdit.setTitle(tool_id); toolConfig = sitePageEdit.addTool(); toolConfig.setTool(tool_id, ToolManager.getTool(tool_id)); toolConfig.setTitle(tool_id); Properties propsedit = toolConfig.getPlacementConfig(); propsedit.setProperty(BASICLTI_RESOURCE_LINK, (String) payload.get(BasicLTIConstants.RESOURCE_LINK_ID)); pushAdvisor(); try { SiteService.save(site); M_log.info("Tool added, tool_id="+tool_id + ", siteId="+site.getId()); } catch (Exception e) { new LTIException( "launch.site.save", "tool_id="+tool_id + ", siteId="+site.getId(), e); } finally { popAdvisor(); } toolPlacementId = toolConfig.getId(); } catch (Exception e) { new LTIException( "launch.tool.add", "tool_id="+tool_id + ", siteId="+site.getId(), e); } } } // Get ToolConfiguration for tool if not already setup if(toolConfig == null){ toolConfig = site.getToolForCommonId(tool_id); } // Check user has access to this tool in this site if(!ToolManager.isVisible(site, toolConfig)) { M_log.warn("Not allowed to access tool user_id=" + user.getId() + " site="+ site.getId() + " tool=" + tool_id); throw new LTIException( "launch.site.tool.denied", "user_id=" + user.getId() + " site="+ site.getId() + " tool=" + tool_id, null); } return toolPlacementId; } private Site addOrUpdateSiteMembership(Map payload, boolean trustedConsumer, User user, Site site) throws LTIException { String userrole = getUserRole(payload, trustedConsumer); // Check if the user is a member of the site already boolean userExistsInSite = false; try { Member member = site.getMember(user.getId()); if(member != null && BasicLTIUtil.equals(member.getUserEid(), user.getEid())) { userExistsInSite = true; } } catch (Exception e) { M_log.warn(e.getLocalizedMessage(), e); throw new LTIException( "launch.site.invalid", "siteId="+site.getId(), e); } if (M_log.isDebugEnabled()) { M_log.debug("userExistsInSite=" + userExistsInSite); } // If not a member of the site, and we are a trusted consumer, error // Otherwise, add them to the site if(!userExistsInSite) { if(trustedConsumer) { throw new LTIException( "launch.site.user.missing", "user_id="+user.getId()+ ", siteId="+site.getId(), null); } else { try { site = SiteService.getSite(site.getId()); Set<Role> roles = site.getRoles(); //BLTI-151 see if we can directly map the incoming role to the list of site roles String newRole = null; if (M_log.isDebugEnabled()) { M_log.debug("Incoming userrole:" + userrole); } for (Role r : roles) { String roleId = r.getId(); if (BasicLTIUtil.equalsIgnoreCase(roleId, userrole)) { newRole = roleId; if (M_log.isDebugEnabled()) { M_log.debug("Matched incoming role to role in site:" + roleId); } break; } } //if we haven't mapped a role, check against the standard roles and fallback if (BasicLTIUtil.isBlank(newRole)) { if (M_log.isDebugEnabled()) { M_log.debug("No match, falling back to determine role"); } String maintainRole = site.getMaintainRole(); String joinerRole = site.getJoinerRole(); for (Role r : roles) { String roleId = r.getId(); if (maintainRole == null && (roleId.equalsIgnoreCase("maintain") || roleId.equalsIgnoreCase("instructor"))) { maintainRole = roleId; } if (joinerRole == null && (roleId.equalsIgnoreCase("access") || roleId.equalsIgnoreCase("student"))) { joinerRole = roleId; } } boolean isInstructor = userrole.indexOf("instructor") >= 0; newRole = joinerRole; if (isInstructor && maintainRole != null) { newRole = maintainRole; } if (M_log.isDebugEnabled()) { M_log.debug("Determined newRole as: " + newRole); } } if (newRole == null) { M_log.warn("Could not find Sakai role, role=" + userrole+ " user=" + user.getId() + " site=" + site.getId()); throw new LTIException( "launch.role.missing", "siteId="+site.getId(), null); } Role currentRoleObject = site.getUserRole(user.getId()); String currentRole = null; if (currentRoleObject != null) { currentRole = currentRoleObject.getId(); } if (!newRole.equals(currentRole)) { site.addMember(user.getId(), newRole, true, false); if (currentRole == null) { M_log.info("Added role=" + newRole + " user=" + user.getId() + " site=" + site.getId() + " LMS Role=" + userrole); } else { M_log.info("Old role=" + currentRole + " New role=" + newRole + " user=" + user.getId() + " site=" + site.getId()+ " LMS Role=" + userrole); } pushAdvisor(); String tool_id = (String) payload.get("tool_id"); try { SiteService.save(site); M_log.info("Site saved role=" + newRole + " user="+ user.getId() + " site=" + site.getId()); } catch (Exception e) { throw new LTIException("launch.site.save", "siteId="+ site.getId() + " tool_id=" + tool_id, e); } finally { popAdvisor(); } } } catch (Exception e) { M_log.warn("Could not add user to site role=" + userrole + " user="+ user.getId() + " site=" + site.getId()); M_log.warn(e.getLocalizedMessage(), e); throw new LTIException( "launch.join.site", "siteId="+site.getId(), e); } } } return site; } protected Site findOrCreateSite(Map payload, boolean trustedConsumer) throws LTIException { String context_id = (String) payload.get(BasicLTIConstants.CONTEXT_ID); String oauth_consumer_key = (String) payload.get("oauth_consumer_key"); String siteId = null; if (trustedConsumer) { siteId = context_id; } else { siteId = ShaUtil.sha1Hash(oauth_consumer_key + ":" + context_id); } if (M_log.isDebugEnabled()) { M_log.debug("siteId=" + siteId); } final String context_title = (String) payload.get(BasicLTIConstants.CONTEXT_TITLE); final String context_label = (String) payload.get(BasicLTIConstants.CONTEXT_LABEL); Site site = null; // Get the site if it exists if (ServerConfigurationService.getBoolean("basiclti.provider.lookupSitesByLTIContextIdProperty", false)) { try { site = findSiteByLTIContextId(context_id); if (site != null) { updateSiteDetailsIfChanged(site, context_title, context_label); return site; } } catch (Exception e) { if (M_log.isDebugEnabled()) { M_log.debug(e.getLocalizedMessage(), e); } } } else { try { site = SiteService.getSite(siteId); updateSiteDetailsIfChanged(site, context_title, context_label); return site; } catch (Exception e) { if (M_log.isDebugEnabled()) { M_log.debug(e.getLocalizedMessage(), e); } } } // If trusted and site does not exist, error, otherwise, create the site if (trustedConsumer) { throw new LTIException("launch.site.invalid", "siteId=" + siteId, null); } else { pushAdvisor(); try { String sakai_type = "project"; // BLTI-154. If an autocreation site template has been specced in sakai.properties, use it. String autoSiteTemplateId = ServerConfigurationService.getString("basiclti.provider.autositetemplate", null); boolean templateSiteExists = SiteService.siteExists(autoSiteTemplateId); if(!templateSiteExists) { M_log.warn("A template site id was specced (" + autoSiteTemplateId + ") but no site with this id exists. A default lti site will be created instead."); } if(autoSiteTemplateId == null || !templateSiteExists) { //BLTI-151 If the new site type has been specified in sakai.properties, use it. sakai_type = ServerConfigurationService.getString("basiclti.provider.newsitetype", null); if(BasicLTIUtil.isBlank(sakai_type)) { // It wasn't specced in the props. Test for the ims course context type. final String context_type = (String) payload.get(BasicLTIConstants.CONTEXT_TYPE); if (BasicLTIUtil.equalsIgnoreCase(context_type, "course")) { sakai_type = "course"; } else { sakai_type = BasicLTIConstants.NEW_SITE_TYPE; } } site = SiteService.addSite(siteId, sakai_type); site.setType(sakai_type); } else { Site autoSiteTemplate = SiteService.getSite(autoSiteTemplateId); site = SiteService.addSite(siteId, autoSiteTemplate); } if (BasicLTIUtil.isNotBlank(context_title)) { site.setTitle(context_title); } if (BasicLTIUtil.isNotBlank(context_label)) { site.setShortDescription(context_label); } site.setJoinable(false); site.setPublished(true); site.setPubView(false); // record the original context_id to a site property site.getPropertiesEdit().addProperty(LTI_CONTEXT_ID, context_id); try { SiteService.save(site); M_log.info("Created site=" + siteId + " label=" + context_label + " type=" + sakai_type + " title=" + context_title); } catch (Exception e) { throw new LTIException("launch.site.save", "siteId=" + siteId, e); } } catch (Exception e) { throw new LTIException("launch.create.site", "siteId=" + siteId, e); } finally { popAdvisor(); } } try { return SiteService.getSite(site.getId()); } catch (IdUnusedException e) { throw new LTIException( "launch.site.invalid", "siteId="+siteId, e); } } private final void updateSiteDetailsIfChanged(Site site, String context_title, String context_label) { boolean changed = false; if (BasicLTIUtil.isNotBlank(context_title) && !context_title.equals(site.getTitle())) { site.setTitle(context_title); changed = true; } if (BasicLTIUtil.isNotBlank(context_label) && !context_label.equals(site.getShortDescription())) { site.setShortDescription(context_label); changed = true; } if(changed) { try { SiteService.save(site); M_log.info("Updated site=" + site.getId() + " title=" + context_title + " label=" + context_label); } catch (Exception e) { M_log.warn("Failed to update site title and/or label"); } } } protected String getEid(Map payload, boolean trustedConsumer, String user_id) throws LTIException { String eid; String oauth_consumer_key = (String) payload.get("oauth_consumer_key"); String ext_sakai_provider_eid = (String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID); if(BasicLTIUtil.isNotBlank(ext_sakai_provider_eid)){ eid = (String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID); } else { if(trustedConsumer) { try { eid = UserDirectoryService.getUserEid(user_id); } catch (Exception e) { M_log.error(e.getLocalizedMessage(), e); throw new LTIException( "launch.user.invalid", "user_id="+user_id, e); } } else { eid = oauth_consumer_key + ":" + user_id; } if (M_log.isDebugEnabled()) { M_log.debug("eid=" + eid); } } return eid; } private String getUserRole(Map payload, boolean trustedConsumer) { // Setup role in the site. If trusted, we don't need this as the user already has a role in the site String userrole = null; if(!trustedConsumer) { userrole = (String) payload.get(BasicLTIConstants.ROLES); if (userrole == null) { userrole = ""; } else { userrole = userrole.toLowerCase(); } } return userrole; } protected User findOrCreateUser(Map payload, boolean trustedConsumer) throws LTIException { User user; String user_id = (String) payload.get(BasicLTIConstants.USER_ID); // Get the eid, either from the value provided or if trusted get it from the user_id,otherwise construct it. String eid = getEid(payload, trustedConsumer, user_id); // If we did not get first and last name, split lis_person_name_full final String fullname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_FULL); String fname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_GIVEN); String lname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_FAMILY); String email = (String) payload.get(BasicLTIConstants.LIS_PERSON_CONTACT_EMAIL_PRIMARY); if (fname == null && lname == null && fullname != null) { int ipos = fullname.trim().lastIndexOf(' '); if (ipos == -1) { fname = fullname; } else { fname = fullname.substring(0, ipos); lname = fullname.substring(ipos + 1); } } // If trusted consumer, login, otherwise check for existing user and create one if required // Note that if trusted, then the user must have already logged into Sakai in order to have an account stub created for them // otherwise this will fail since they don't exist. Perhaps this should be addressed? if (trustedConsumer) { try { user = UserDirectoryService.getUser(user_id); } catch (UserNotDefinedException e) { throw new LTIException("launch.user.invalid", "user_id=" + user_id, e); } } else { try { user = UserDirectoryService.getUserByEid(eid); } catch (Exception e) { if (M_log.isDebugEnabled()) { M_log.debug(e.getLocalizedMessage(), e); } user = null; } if (user == null) { try { String hiddenPW = IdManager.createUuid(); UserDirectoryService.addUser(null, eid, fname, lname, email, hiddenPW, "registered", null); M_log.info("Created user=" + eid); user = UserDirectoryService.getUserByEid(eid); } catch (Exception e) { throw new LTIException("launch.create.user", "user_id=" + user_id, e); } } // post the login event // eventTrackingService().post(eventTrackingService().newEvent(EVENT_LOGIN, // null, true)); } return user; } private void loginUser(String ipAddress, User user) { Session sess = SessionManager.getCurrentSession(); UsageSessionService.login(user.getId(), user.getEid(), ipAddress, null, UsageSessionService.EVENT_LOGIN_WS); sess.setUserId(user.getId()); sess.setUserEid(user.getEid()); } /** * BLTI-155. If Profile2 is installed, set the profile picture to the user_image url, if supplied. * * @param payload The LTI launch parameters in a Map * @param user The provisioned user who MUST be already logged in. * @param isTrustedConsumer If this is true, do nothing as we assume that a local * user corresponding to the consumer user already exists */ private void setupUserPicture(Map payload, User user, boolean isTrustedConsumer) { if(isTrustedConsumer) return; String imageUrl = (String) payload.get(BasicLTIConstants.USER_IMAGE); if(imageUrl != null && imageUrl.length() > 0) { M_log.debug("User image supplied by consumer: " + imageUrl); if(saveOfficialImageUrlMethod != null && getPreferencesRecordForUserMethod != null && setUseOfficialImageMethod != null && savePreferencesRecordMethod != null) { try { saveOfficialImageUrlMethod.invoke(profileImageLogicObject, new Object[] {user.getId(), imageUrl}); Object prefs = getPreferencesRecordForUserMethod.invoke(profilePreferencesLogicObject, new String [] { user.getId() }); setUseOfficialImageMethod.invoke(prefs,new Object[] { true }); savePreferencesRecordMethod.invoke(profilePreferencesLogicObject,new Object[] { prefs }); } catch(Exception e) { M_log.error("Failed to setup launcher's Profile2 picture.",e); } } } } private void setupUserLocale(Map payload, User user, boolean isTrustedConsumer) { if(isTrustedConsumer) return; // BLTI-153. Set up user's language. String locale = (String) payload.get(BasicLTIConstants.LAUNCH_PRESENTATION_LOCALE); if(locale != null && locale.length() > 0) { try { PreferencesEdit pe = null; try { pe = PreferencesService.edit(user.getId()); } catch(IdUnusedException idue) { pe = PreferencesService.add(user.getId()); } ResourcePropertiesEdit propsEdit = pe.getPropertiesEdit("sakai:resourceloader"); propsEdit.removeProperty(Preferences.FIELD_LOCALE); propsEdit.addProperty(Preferences.FIELD_LOCALE,locale); PreferencesService.commit(pe); } catch(Exception e) { M_log.error("Failed to setup launcher's locale",e); } } } /** * Picks up the ext_email_delivery_preference parameter, if supplied, and reflects it in the user's preferences * as a site override for the tool being launched. This is *not* an official part of the LTI 1.1 spec; this * functionality will probably become part of a LTI 2.0 consumer preferences service. */ private void setupUserEmailPreferenceForSite(Map payload, User user, Site site, boolean isTrustedConsumer) { if(isTrustedConsumer) return; // Set up user's email preference. String emailDeliveryPreference = (String) payload.get("ext_email_delivery_preference"); if(emailDeliveryPreference != null && emailDeliveryPreference.length() > 0) { try { PreferencesEdit pe = null; try { pe = PreferencesService.edit(user.getId()); } catch(IdUnusedException idue) { pe = PreferencesService.add(user.getId()); } if(emailDeliveryPreference != null && emailDeliveryPreference.length() > 0) { int notificationPref = NotificationService.PREF_IMMEDIATE; if(emailDeliveryPreference.equals("none")) { notificationPref = NotificationService.PREF_NONE; } else if(emailDeliveryPreference.equals("digest")) { notificationPref = NotificationService.PREF_DIGEST; } String toolId = ((String) payload.get("tool_id")).replaceFirst("\\.",":"); ResourcePropertiesEdit propsEdit = pe.getPropertiesEdit(NotificationService.PREFS_TYPE + toolId + "_override"); propsEdit.removeProperty(site.getId()); propsEdit.addProperty(site.getId(), Integer.toString(notificationPref)); } PreferencesService.commit(pe); } catch(Exception e) { M_log.error("Failed to setup launcher's locale and/or email preference.",e); } } } protected boolean isTrustedConsumer(Map payload) { boolean isTrustedConsumer = false; String oauth_consumer_key = (String) payload.get("oauth_consumer_key"); final String trustedConsumersConfig = ServerConfigurationService .getString("basiclti.provider.highly.trusted.consumers", null); if(BasicLTIUtil.isNotBlank(trustedConsumersConfig)) { String[] trustedConsumers = trustedConsumersConfig.split(":"); List<String> trustedConsumersList = Arrays.asList(trustedConsumers); if (trustedConsumersList.contains(oauth_consumer_key)) { isTrustedConsumer = true; } } if (M_log.isDebugEnabled()) { M_log.debug("Consumer=" + oauth_consumer_key); M_log.debug("Trusted=" + isTrustedConsumer); } return isTrustedConsumer; } public void destroy() { } public Site findSiteByLTIContextId(String externalOaeId) throws Exception { Map propertyCriteria = new HashMap(); // Replace search property propertyCriteria.put(LTI_CONTEXT_ID, externalOaeId); List list = SiteService.getSites(org.sakaiproject.site.api.SiteService.SelectionType.ANY, null, null, propertyCriteria, org.sakaiproject.site.api.SiteService.SortType.NONE, null); if (list != null && list.size() > 0) { for (Iterator i=list.iterator(); i.hasNext();) { Site site = (Site) i.next(); if (site.getProperties() != null) { String loadedExternalSiteId = (String) site.getProperties().get(LTI_CONTEXT_ID); if (loadedExternalSiteId != null && loadedExternalSiteId.equals(externalOaeId)) { // deeply load site, otherwise groups won't be loaded M_log.debug("found site: " + site.getId() + " with lti_context_id:" + externalOaeId); return SiteService.getSite(site.getId()); } } } } return null; } }