/** * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiBLTIUtil.java $ * $Id: SakaiBLTIUtil.java 132745 2013-12-18 16:29:22Z csev@umich.edu $ * * Copyright (c) 2006-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.basiclti.util; import java.util.Properties; import java.util.Map; import java.util.TreeMap; import java.util.List; import java.util.Iterator; import java.util.Enumeration; import java.net.URL; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.imsglobal.basiclti.BasicLTIUtil; import org.imsglobal.basiclti.BasicLTIConstants; import org.imsglobal.lti2.LTI2Constants; import org.imsglobal.lti2.LTI2Util; import org.sakaiproject.lti.api.LTIService; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.user.api.User; import org.sakaiproject.user.cover.UserDirectoryService; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.tool.api.Placement; import org.sakaiproject.authz.cover.SecurityService; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.api.privacy.PrivacyManager; import org.sakaiproject.authz.cover.AuthzGroupService; import org.sakaiproject.authz.api.AuthzGroup; import org.sakaiproject.authz.api.Role; import org.sakaiproject.authz.api.Member; import org.sakaiproject.authz.api.GroupNotDefinedException; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.util.Web; import org.sakaiproject.portal.util.CSSUtils; import org.sakaiproject.linktool.LinkToolUtil; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.cover.SecurityService; import org.sakaiproject.service.gradebook.shared.AssignmentHasIllegalPointsException; import org.sakaiproject.service.gradebook.shared.CategoryDefinition; import org.sakaiproject.service.gradebook.shared.GradebookService; import org.sakaiproject.service.gradebook.shared.GradebookExternalAssessmentService; import org.sakaiproject.service.gradebook.shared.ConflictingAssignmentNameException; import org.sakaiproject.service.gradebook.shared.ConflictingExternalIdException; import org.sakaiproject.service.gradebook.shared.GradebookNotFoundException; import org.sakaiproject.service.gradebook.shared.Assignment; import org.sakaiproject.service.gradebook.shared.CommentDefinition; import net.oauth.OAuthAccessor; import net.oauth.OAuthConsumer; import net.oauth.OAuthMessage; import net.oauth.OAuthValidator; import net.oauth.SimpleOAuthValidator; import net.oauth.signature.OAuthSignatureMethod; /** * Some Sakai Utility code for IMS Basic LTI * This is mostly code to support the Sakai conventions for * making and launching BLTI resources within Sakai. */ @SuppressWarnings("deprecation") public class SakaiBLTIUtil { private static Log M_log = LogFactory.getLog(SakaiBLTIUtil.class); public static final boolean verbosePrint = false; public static final String BASICLTI_OUTCOMES_ENABLED = "basiclti.outcomes.enabled"; public static final String BASICLTI_OUTCOMES_ENABLED_DEFAULT = "true"; public static final String BASICLTI_SETTINGS_ENABLED = "basiclti.settings.enabled"; public static final String BASICLTI_SETTINGS_ENABLED_DEFAULT = "true"; public static final String BASICLTI_ROSTER_ENABLED = "basiclti.roster.enabled"; public static final String BASICLTI_ROSTER_ENABLED_DEFAULT = "true"; public static final String BASICLTI_LORI_ENABLED = "basiclti.lori.enabled"; public static final String BASICLTI_LORI_ENABLED_DEFAULT = "true"; public static final String BASICLTI_CONTENTLINK_ENABLED = "basiclti.contentlink.enabled"; public static final String BASICLTI_CONTENTLINK_ENABLED_DEFAULT = null; // i.e. false public static final String BASICLTI_CONSUMER_USERIMAGE_ENABLED = "basiclti.consumer.userimage.enabled"; public static final String BASICLTI_ENCRYPTION_KEY = "basiclti.encryption.key"; public static final String SVC_tc_profile = "tc_profile"; public static final String SVC_tc_registration = "tc_registration"; public static final String SVC_Settings = "Settings"; public static final String SVC_Result = "Result"; public static final String LTI1_PATH = "/imsblis/service/"; public static final String LTI2_PATH = "/imsblis/lti2/"; public static void dPrint(String str) { if ( verbosePrint ) System.out.println(str); } // Retrieve the property from the configuration unless it // is overridden by the server configurtation (i.e. sakai.properties) public static String getCorrectProperty(Properties config, String propName, Placement placement) { // Check for global overrides in properties String allowSettings = ServerConfigurationService.getString(BASICLTI_SETTINGS_ENABLED, BASICLTI_SETTINGS_ENABLED_DEFAULT); if ( LTIService.LTI_ALLOWSETTINGS.equals(propName) && ! "true".equals(allowSettings) ) return "false"; String allowRoster = ServerConfigurationService.getString(BASICLTI_ROSTER_ENABLED, BASICLTI_ROSTER_ENABLED_DEFAULT); if ( LTIService.LTI_ALLOWROSTER.equals(propName) && ! "true".equals(allowRoster) ) return "false"; String allowLori = ServerConfigurationService.getString(BASICLTI_LORI_ENABLED, BASICLTI_LORI_ENABLED_DEFAULT); if ( LTIService.LTI_ALLOWLORI.equals(propName) && ! "true".equals(allowLori) ) return "false"; String allowContentLink = ServerConfigurationService.getString(BASICLTI_CONTENTLINK_ENABLED, BASICLTI_CONTENTLINK_ENABLED_DEFAULT); if ( "contentlink".equals(propName) && ! "true".equals(allowContentLink) ) return null; // Check for explicit setting in properties String propertyName = placement.getToolId() + "." + propName; String propValue = ServerConfigurationService.getString(propertyName,null); if ( propValue != null && propValue.trim().length() > 0 ) { // System.out.println("Sakai.home "+propName+"="+propValue); return propValue; } // Take it from the placement return config.getProperty("imsti."+propName, null); } // Look at a Placement and come up with the launch urls, and // other launch parameters to drive the launch. public static boolean loadFromPlacement(Properties info, Properties launch, Placement placement) { Properties config = placement.getConfig(); dPrint("Sakai properties=" + config); String launch_url = toNull(getCorrectProperty(config,LTIService.LTI_LAUNCH, placement)); setProperty(info, "launch_url", launch_url); if ( launch_url == null ) { String xml = toNull(getCorrectProperty(config,"xml", placement)); if ( xml == null ) return false; BasicLTIUtil.parseDescriptor(info, launch, xml); } String secret = getCorrectProperty(config,LTIService.LTI_SECRET, placement); // BLTI-195 - Compatibility mode for old-style encrypted secrets if ( secret == null || secret.trim().length() < 1 ) { String eSecret = getCorrectProperty(config,"encryptedsecret", placement); if ( eSecret != null && eSecret.trim().length() > 0 ) { secret = eSecret.trim() + ":" + SimpleEncryption.CIPHER; } } setProperty(info, LTIService.LTI_SECRET, secret ); // This is not "consumerkey" on purpose - we are mimicking the old placement model setProperty(info, "key", getCorrectProperty(config,"key", placement) ); setProperty(info, LTIService.LTI_DEBUG, getCorrectProperty(config,LTIService.LTI_DEBUG, placement) ); setProperty(info, LTIService.LTI_FRAMEHEIGHT, getCorrectProperty(config,LTIService.LTI_FRAMEHEIGHT, placement) ); setProperty(info, LTIService.LTI_NEWPAGE, getCorrectProperty(config,LTIService.LTI_NEWPAGE, placement) ); setProperty(info, LTIService.LTI_TITLE, getCorrectProperty(config,"tooltitle", placement) ); // Pull in and parse the custom parameters String customstr = toNull(getCorrectProperty(config,LTIService.LTI_CUSTOM, placement) ); parseCustom(info, customstr); if ( info.getProperty("launch_url", null) != null || info.getProperty("secure_launch_url", null) != null ) { return true; } return false; } public static void parseCustom(Properties info, String customstr) { if ( customstr != null ) { String [] params = customstr.split("[\n;]"); for (int i = 0 ; i < params.length; i++ ) { String param = params[i]; if ( param == null ) continue; if ( param.length() < 1 ) continue; int pos = param.indexOf("="); if ( pos < 1 ) continue; if ( pos+1 > param.length() ) continue; String key = BasicLTIUtil.mapKeyName(param.substring(0,pos)); if ( key == null ) continue; String value = param.substring(pos+1); if ( value == null ) continue; value = value.trim(); if ( value.length() < 1 ) continue; setProperty(info, "custom_"+key, value); } } } public static String encryptSecret(String orig) { if ( orig == null || orig.trim().length() < 1 ) return orig; String encryptionKey = ServerConfigurationService.getString(BASICLTI_ENCRYPTION_KEY, null); if ( encryptionKey == null ) return orig; // May throw runtime exception - just let it log as this is abnormal... String newsecret = SimpleEncryption.encrypt(encryptionKey, orig); return newsecret; } public static String decryptSecret(String orig) { if ( orig == null || orig.trim().length() < 1 ) return orig; String encryptionKey = ServerConfigurationService.getString(BASICLTI_ENCRYPTION_KEY, null); if ( encryptionKey == null ) return orig; try { String newsecret = SimpleEncryption.decrypt(encryptionKey, orig); return newsecret; } catch (RuntimeException re) { dPrint("Exception when decrypting secret - this is normal if the secret is unencrypted"); return orig; } } public static boolean sakaiInfo(Properties props, Placement placement, ResourceLoader rb) { dPrint("placement="+ placement.getId()); dPrint("placement title=" + placement.getTitle()); String context = placement.getContext(); dPrint("ContextID="+context); return sakaiInfo(props, context, placement.getId(), rb); } public static void addSiteInfo(Properties props, Properties lti2subst, Site site) { if ( site != null ) { String context_type = site.getType(); if ( context_type != null && context_type.toLowerCase().contains("course") ){ setProperty(props,BasicLTIConstants.CONTEXT_TYPE,BasicLTIConstants.CONTEXT_TYPE_COURSE_SECTION); } setProperty(props,BasicLTIConstants.CONTEXT_ID,site.getId()); setProperty(lti2subst,"CourseOffering.id",site.getId()); setProperty(props,BasicLTIConstants.CONTEXT_LABEL,site.getTitle()); setProperty(lti2subst,"CourseOffering.label",site.getTitle()); setProperty(props,BasicLTIConstants.CONTEXT_TITLE,site.getTitle()); setProperty(lti2subst,"CourseOffering.title",site.getTitle()); String courseRoster = getExternalRealmId(site.getId()); if ( courseRoster != null ) { setProperty(props,BasicLTIConstants.LIS_COURSE_OFFERING_SOURCEDID,courseRoster); setProperty(lti2subst,"CourseOffering.sourcedId",courseRoster); } } // Fix up the return Url String returnUrl = ServerConfigurationService.getString("basiclti.consumer_return_url",null); if ( returnUrl == null ) { returnUrl = getOurServerUrl() + LTI1_PATH + "return-url"; Session s = SessionManager.getCurrentSession(); if (s != null) { String controllingPortal = (String) s.getAttribute("sakai-controlling-portal"); if ( controllingPortal == null ) { returnUrl = returnUrl + "/site"; } else { returnUrl = returnUrl + "/" + controllingPortal; } } returnUrl = returnUrl + "/" + site.getId(); } setProperty(props, BasicLTIConstants.LAUNCH_PRESENTATION_RETURN_URL, returnUrl); } public static void addRoleInfo(Properties props, Properties lti2subst, String context) { String theRole = "Learner"; if ( SecurityService.isSuperUser() ) { theRole = "Instructor,Administrator,urn:lti:instrole:ims/lis/Administrator,urn:lti:sysrole:ims/lis/Administrator"; } else if ( SiteService.allowUpdateSite(context) ) { theRole = "Instructor"; } setProperty(props,BasicLTIConstants.ROLES,theRole); setProperty(lti2subst,"Membership.role",theRole); String realmId = SiteService.siteReference(context); try { User user = UserDirectoryService.getCurrentUser(); if ( user != null ) { Role role = null; String roleId = null; AuthzGroup realm = AuthzGroupService.getAuthzGroup(realmId); if ( realm != null ) role = realm.getUserRole(user.getId()); if ( role != null ) roleId = role.getId(); if ( roleId != null && roleId.length() > 0 ) setProperty(props, "ext_sakai_role", roleId); } } catch (GroupNotDefinedException e) { dPrint("SiteParticipantHelper.getExternalRealmId: site realm not found"+e.getMessage()); } } // Retrieve the Sakai information about users, etc. public static boolean sakaiInfo(Properties props, String context, String placementId, ResourceLoader rb) { Site site = null; try { site = SiteService.getSite(context); } catch (Exception e) { dPrint("No site/page associated with Launch context="+context); return false; } // Add the generic information addGlobalData(site, props, null, rb); addRoleInfo(props, null, context); addSiteInfo(props, null, site); // Add Placement Information addPlacementInfo(props, placementId); return true; } public static void addPlacementInfo(Properties props, String placementId) { // Get the placement to see if we are to release information ToolConfiguration placement = SiteService.findTool(placementId); Properties config = placement.getConfig(); // Start setting the Basici LTI parameters setProperty(props,BasicLTIConstants.RESOURCE_LINK_ID,placementId); String pagetitle = toNull(getCorrectProperty(config,LTIService.LTI_PAGETITLE, placement)); if ( pagetitle != null ) setProperty(props,BasicLTIConstants.RESOURCE_LINK_TITLE,pagetitle); String tooltitle = toNull(getCorrectProperty(config,"tooltitle", placement)); if ( tooltitle != null ) setProperty(props,BasicLTIConstants.RESOURCE_LINK_DESCRIPTION,tooltitle); String releasename = toNull(getCorrectProperty(config,"releasename", placement)); String releaseemail = toNull(getCorrectProperty(config,"releaseemail", placement)); User user = UserDirectoryService.getCurrentUser(); PrivacyManager pm = (PrivacyManager) ComponentManager.get("org.sakaiproject.api.privacy.PrivacyManager"); // TODO: Think about anonymous if ( user != null ) { String context = placement.getContext(); boolean isViewable = pm.isViewable("/site/" + context, user.getId()); setProperty(props,"ext_sakai_privacy", isViewable ? "visible" : "hidden"); setProperty(props,BasicLTIConstants.USER_ID,user.getId()); if(ServerConfigurationService.getBoolean(BASICLTI_CONSUMER_USERIMAGE_ENABLED, true)) { String imageUrl = getOurServerUrl() + "/direct/profile/" + user.getId() + "/image"; setProperty(props,BasicLTIConstants.USER_IMAGE,imageUrl); } if ( "on".equals(releasename) ) { setProperty(props,BasicLTIConstants.LIS_PERSON_NAME_GIVEN,user.getFirstName()); setProperty(props,BasicLTIConstants.LIS_PERSON_NAME_FAMILY,user.getLastName()); setProperty(props,BasicLTIConstants.LIS_PERSON_NAME_FULL,user.getDisplayName()); } if ( "on".equals(releaseemail) ) { setProperty(props,BasicLTIConstants.LIS_PERSON_CONTACT_EMAIL_PRIMARY,user.getEmail()); setProperty(props,BasicLTIConstants.LIS_PERSON_SOURCEDID,user.getEid()); setProperty(props,"ext_sakai_eid",user.getEid()); } String assignment = null; // It is a little tricky - the tool configuration on/off decides whether // We check the serverCongigurationService true/false // We use the tool configuration to force outcomes off regardless of // server settings (i.e. an external tool never wants the outcomes // UI shown because it simply does not handle outcomes). String allowOutcomes = toNull(getCorrectProperty(config,LTIService.LTI_ALLOWOUTCOMES, placement)); if ( ! "off".equals(allowOutcomes) ) { assignment = toNull(getCorrectProperty(config,"assignment", placement)); allowOutcomes = ServerConfigurationService.getString( BASICLTI_OUTCOMES_ENABLED, BASICLTI_OUTCOMES_ENABLED_DEFAULT); if ( ! "true".equals(allowOutcomes) ) allowOutcomes = null; } String allowSettings = toNull(getCorrectProperty(config,LTIService.LTI_ALLOWSETTINGS, placement)); if ( ! "on".equals(allowSettings) ) allowSettings = null; String allowRoster = toNull(getCorrectProperty(config,LTIService.LTI_ALLOWROSTER, placement)); if ( ! "on".equals(allowRoster) ) allowRoster = null; String allowLori = toNull(getCorrectProperty(config,LTIService.LTI_ALLOWLORI, placement)); if ( ! "on".equals(allowLori) ) allowLori = null; String result_sourcedid = getSourceDID(user, placement, config); if ( result_sourcedid != null ) { if ( "true".equals(allowOutcomes) && assignment != null ) { setProperty(props,BasicLTIConstants.LIS_RESULT_SOURCEDID, result_sourcedid); // New Basic Outcomes URL String outcome_url = ServerConfigurationService.getString("basiclti.consumer.ext_ims_lis_basic_outcome_url",null); if ( outcome_url == null ) outcome_url = getOurServerUrl() + LTI1_PATH; setProperty(props,"ext_ims_lis_basic_outcome_url", outcome_url); outcome_url = ServerConfigurationService.getString("basiclti.consumer."+BasicLTIConstants.LIS_OUTCOME_SERVICE_URL,null); if ( outcome_url == null ) outcome_url = getOurServerUrl() + LTI1_PATH; setProperty(props,BasicLTIConstants.LIS_OUTCOME_SERVICE_URL, outcome_url); } if ( "on".equals(allowSettings) ) { setProperty(props,"ext_ims_lti_tool_setting_id", result_sourcedid); String setting = config.getProperty("toolsetting", null); if ( setting != null ) { setProperty(props,"ext_ims_lti_tool_setting", setting); } String service_url = ServerConfigurationService.getString("basiclti.consumer.ext_ims_lti_tool_setting_url",null); if ( service_url == null ) service_url = getOurServerUrl() + LTI1_PATH; setProperty(props,"ext_ims_lti_tool_setting_url", service_url); } if ( "on".equals(allowRoster) ) { setProperty(props,"ext_ims_lis_memberships_id", result_sourcedid); String roster_url = ServerConfigurationService.getString("basiclti.consumer.ext_ims_lis_memberships_url",null); if ( roster_url == null ) roster_url = getOurServerUrl() + LTI1_PATH; setProperty(props,"ext_ims_lis_memberships_url", roster_url); } if ( "on".equals(allowLori) ) { setProperty(props,"ext_lori_api_token", result_sourcedid); setProperty(props,BasicLTIConstants.LIS_RESULT_SOURCEDID, result_sourcedid); String lori_url = ServerConfigurationService.getString("basiclti.consumer.ext_lori_api_url",null); if ( lori_url == null ) lori_url = getOurServerUrl() + LTI1_PATH; String lori_url_xml = ServerConfigurationService.getString("basiclti.consumer.ext_lori_api_url_xml",null); if ( lori_url_xml == null ) lori_url_xml = getOurServerUrl() + LTI1_PATH; setProperty(props,"ext_lori_api_url", lori_url); setProperty(props,"ext_lori_api_url_xml", lori_url_xml); } } } // Send along the content link String contentlink = toNull(getCorrectProperty(config,"contentlink", placement)); if ( contentlink != null ) setProperty(props,"ext_resource_link_content",contentlink); // Send along the signed session if requested String sendsession = toNull(getCorrectProperty(config,"ext_sakai_session", placement)); if ( "true".equals(sendsession) ) { Session s = SessionManager.getCurrentSession(); if (s != null) { String sessionid = s.getId(); if (sessionid != null) { sessionid = LinkToolUtil.encrypt(sessionid); setProperty(props,"ext_sakai_session",sessionid); } } } } public static void addGlobalData(Site site, Properties props, Properties custom, ResourceLoader rb) { if ( rb != null ) setProperty(props,BasicLTIConstants.LAUNCH_PRESENTATION_LOCALE,rb.getLocale().toString()); // Get the organizational information setProperty(props,BasicLTIConstants.TOOL_CONSUMER_INSTANCE_GUID, ServerConfigurationService.getString("basiclti.consumer_instance_guid",null)); setProperty(props,BasicLTIConstants.TOOL_CONSUMER_INSTANCE_NAME, ServerConfigurationService.getString("basiclti.consumer_instance_name",null)); setProperty(props,BasicLTIConstants.TOOL_CONSUMER_INSTANCE_DESCRIPTION, ServerConfigurationService.getString("basiclti.consumer_instance_description",null)); setProperty(props,BasicLTIConstants.TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL, ServerConfigurationService.getString("basiclti.consumer_instance_contact_email",null)); setProperty(props,BasicLTIConstants.TOOL_CONSUMER_INSTANCE_URL, ServerConfigurationService.getString("basiclti.consumer_instance_url",null)); // Send along the CSS URL String tool_css = ServerConfigurationService.getString("basiclti.consumer.launch_presentation_css_url",null); if ( tool_css == null ) tool_css = getOurServerUrl() + CSSUtils.getCssToolBase(); setProperty(props,BasicLTIConstants.LAUNCH_PRESENTATION_CSS_URL, tool_css); // Send along the CSS URL list String tool_css_all = ServerConfigurationService.getString("basiclti.consumer.ext_sakai_launch_presentation_css_url_all",null); if ( tool_css_all == null ) { tool_css_all = getOurServerUrl() + CSSUtils.getCssToolBase() + ',' + getOurServerUrl() + CSSUtils.getCssToolSkin(site); } setProperty(props,"ext_sakai_" + BasicLTIConstants.LAUNCH_PRESENTATION_CSS_URL + "_list", tool_css_all); // Let tools know we are coming from Sakai String sakaiVersion = ServerConfigurationService.getString("version.sakai","2"); setProperty(props,"ext_lms", "sakai-"+sakaiVersion); setProperty(props,BasicLTIConstants.TOOL_CONSUMER_INFO_PRODUCT_FAMILY_CODE, "sakai"); setProperty(props,BasicLTIConstants.TOOL_CONSUMER_INFO_VERSION, sakaiVersion); // We pass this along in the Sakai world - it might // might be useful to the external tool String serverId = ServerConfigurationService.getServerId(); setProperty(props,"ext_sakai_serverid",serverId); setProperty(props,"ext_sakai_server",getOurServerUrl()); } // getProperty(String name); // Gnerate HTML from a descriptor and properties from public static String[] postLaunchHTML(String descriptor, String contextId, String resourceId, ResourceProperties props, ResourceLoader rb) { if ( descriptor == null || contextId == null || resourceId == null ) return postError("<p>" + getRB(rb, "error.descriptor" ,"Error, missing contextId, resourceid or descriptor")+"</p>" ); // Add user, course, etc to the launch parameters Properties launch = new Properties(); if ( ! sakaiInfo(launch, contextId, resourceId, rb) ) { return postError("<p>" + getRB(rb, "error.info.resource", "Error, cannot load Sakai information for resource=")+resourceId+".</p>"); } Properties info = new Properties(); if ( ! BasicLTIUtil.parseDescriptor(info, launch, descriptor) ) { return postError("<p>" + getRB(rb, "error.badxml.resource", "Error, cannot parse descriptor for resource=")+resourceId+".</p>"); } return postLaunchHTML(info, launch, rb); } // This must return an HTML message as the [0] in the array // If things are successful - the launch URL is in [1] public static String[] postLaunchHTML(Map<String, Object> content, Map<String,Object> tool, LTIService ltiService, ResourceLoader rb) { if ( content == null ) { return postError("<p>" + getRB(rb, "error.content.missing" ,"Content item is missing or improperly configured.")+"</p>" ); } if ( tool == null ) { return postError("<p>" + getRB(rb, "error.tool.missing" ,"Tool item is missing or improperly configured.")+"</p>" ); } int status = getInt(tool.get(LTIService.LTI_STATUS)); if ( status == 1 ) return postError("<p>" + getRB(rb, "tool.disabled" ,"Tool is currently disabled")+"</p>" ); // Go with the content url first String launch_url = (String) content.get(LTIService.LTI_LAUNCH); if ( launch_url == null ) launch_url = (String) tool.get(LTIService.LTI_LAUNCH); if ( launch_url == null ) return postError("<p>" + getRB(rb, "error.nolaunch" ,"This tool is not yet configured.")+"</p>" ); String context = (String) content.get(LTIService.LTI_SITE_ID); Site site = null; try { site = SiteService.getSite(context); } catch (Exception e) { dPrint("No site/page associated with Launch context="+context); return postError("<p>" + getRB(rb, "error.site.missing" ,"Cannot load site.")+context+"</p>" ); } // Percolate up to get the other objects... Map<String, Object> proxyBinding = null; Map<String, Object> deploy = null; Long deployKey = getLongKey(tool.get(LTIService.LTI_DEPLOYMENT_ID)); if ( deployKey >= 0 ) { deploy = ltiService.getDeployDao(deployKey); } Long toolKey = getLongKey(tool.get(LTIService.LTI_ID)); proxyBinding = ltiService.getProxyBindingDao(toolKey,context); Long toolVersion = getLongNull(tool.get(LTIService.LTI_VERSION)); boolean isLTI1 = toolVersion == null || toolVersion == LTIService.LTI_VERSION_1; // Start building up the properties Properties ltiProps = new Properties(); Properties toolProps = new Properties(); Properties lti2subst = new Properties(); if ( isLTI1 ) { setProperty(ltiProps,BasicLTIConstants.LTI_VERSION,BasicLTIConstants.LTI_VERSION_1); } else { setProperty(ltiProps,BasicLTIConstants.LTI_VERSION,BasicLTIConstants.LTI_VERSION_2); } addGlobalData(site, ltiProps, lti2subst, rb); addSiteInfo(ltiProps, lti2subst, site); addRoleInfo(ltiProps, lti2subst, context); if ( deploy != null ) { setProperty(lti2subst,"ToolConsumerProfile.url", getOurServerUrl() + LTI2_PATH + SVC_tc_profile + "/" + (String) deploy.get(LTIService.LTI_CONSUMERKEY));; } String resource_link_id = "content:"+content.get(LTIService.LTI_ID); setProperty(ltiProps,BasicLTIConstants.RESOURCE_LINK_ID,resource_link_id); setProperty(lti2subst,"ResourceLink.id",resource_link_id); setProperty(toolProps, "launch_url", launch_url); String secret = (String) content.get(LTIService.LTI_SECRET); if ( secret == null ) secret = (String) tool.get(LTIService.LTI_SECRET); String key = (String) content.get(LTIService.LTI_CONSUMERKEY); if ( key == null ) key = (String) tool.get(LTIService.LTI_CONSUMERKEY); if ( LTIService.LTI_SECRET_INCOMPLETE.equals(key) && LTIService.LTI_SECRET_INCOMPLETE.equals(secret) ) { return postError("<p>" + getRB(rb, "error.tool.partial" ,"Tool item is incomplete, missing a key and secret.")+"</p>" ); } setProperty(toolProps, LTIService.LTI_SECRET, secret ); setProperty(toolProps, "key", key ); int debug = getInt(tool.get(LTIService.LTI_DEBUG)); if ( debug == 2 ) debug = getInt(content.get(LTIService.LTI_DEBUG)); setProperty(toolProps, LTIService.LTI_DEBUG, debug+""); int frameheight = getInt(tool.get(LTIService.LTI_FRAMEHEIGHT)); if ( frameheight == 2 ) frameheight = getInt(content.get(LTIService.LTI_FRAMEHEIGHT)); setProperty(toolProps, LTIService.LTI_FRAMEHEIGHT, frameheight+"" ); int newpage = getInt(tool.get(LTIService.LTI_NEWPAGE)); if ( newpage == 2 ) newpage = getInt(content.get(LTIService.LTI_NEWPAGE)); setProperty(toolProps, LTIService.LTI_NEWPAGE, newpage+"" ); String title = (String) content.get(LTIService.LTI_TITLE); if ( title == null ) title = (String) tool.get(LTIService.LTI_TITLE); if ( title != null ) { setProperty(ltiProps,BasicLTIConstants.RESOURCE_LINK_TITLE,title); setProperty(lti2subst,"ResourceLink.title",title); } int releasename = getInt(tool.get(LTIService.LTI_SENDNAME)); int releaseemail = getInt(tool.get(LTIService.LTI_SENDEMAILADDR)); User user = UserDirectoryService.getCurrentUser(); if ( user != null ) { setProperty(ltiProps,BasicLTIConstants.USER_ID,user.getId()); setProperty(lti2subst,"User.id",user.getId()); setProperty(ltiProps,BasicLTIConstants.LIS_PERSON_SOURCEDID,user.getEid()); setProperty(lti2subst,"User.username",user.getEid()); if ( releasename == 1 ) { setProperty(ltiProps,BasicLTIConstants.LIS_PERSON_NAME_GIVEN,user.getFirstName()); setProperty(ltiProps,BasicLTIConstants.LIS_PERSON_NAME_FAMILY,user.getLastName()); setProperty(ltiProps,BasicLTIConstants.LIS_PERSON_NAME_FULL,user.getDisplayName()); setProperty(lti2subst,"Person.name.given",user.getFirstName()); setProperty(lti2subst,"Person.name.family",user.getLastName()); setProperty(lti2subst,"Person.name.full",user.getDisplayName()); } if ( releaseemail == 1 ) { setProperty(ltiProps,BasicLTIConstants.LIS_PERSON_CONTACT_EMAIL_PRIMARY,user.getEmail()); setProperty(lti2subst,"Person.email.primary",user.getEmail()); // Only send the display ID if it's different to the EID. if (!user.getEid().equals(user.getDisplayId())) { setProperty(ltiProps,BasicLTIConstants.EXT_SAKAI_PROVIDER_DISPLAYID,user.getDisplayId()); } } } int allowoutcomes = getInt(tool.get(LTIService.LTI_ALLOWOUTCOMES)); int allowroster = getInt(tool.get(LTIService.LTI_ALLOWROSTER)); int allowsettings = getInt(tool.get(LTIService.LTI_ALLOWSETTINGS)); int allowlori = getInt(tool.get(LTIService.LTI_ALLOWLORI)); String placement_secret = (String) content.get(LTIService.LTI_PLACEMENTSECRET); // int tool_id = getInt(tool.get(LTIService.LTI_ID)); String result_sourcedid = getSourceDID(user, resource_link_id, placement_secret); if ( result_sourcedid != null ) { if ( allowoutcomes == 1 ) { setProperty(ltiProps,BasicLTIConstants.LIS_RESULT_SOURCEDID, result_sourcedid); // New Basic Outcomes URL String outcome_url = ServerConfigurationService.getString("basiclti.consumer.ext_ims_lis_basic_outcome_url",null); if ( outcome_url == null ) outcome_url = getOurServerUrl() + LTI1_PATH; setProperty(ltiProps,"ext_ims_lis_basic_outcome_url", outcome_url); outcome_url = ServerConfigurationService.getString("basiclti.consumer."+BasicLTIConstants.LIS_OUTCOME_SERVICE_URL,null); if ( outcome_url == null ) outcome_url = getOurServerUrl() + LTI1_PATH; setProperty(ltiProps,BasicLTIConstants.LIS_OUTCOME_SERVICE_URL, outcome_url); String result_url = getOurServerUrl() + LTI2_PATH + SVC_Result + "/" + result_sourcedid; setProperty(lti2subst, "Result.url", result_url); } // We don't allow LTI 2 tools to have access to the old settings extension // because they can use it to set it to non-JSON if ( allowsettings == 1 ) { if ( isLTI1 ) { setProperty(ltiProps,"ext_ims_lti_tool_setting_id", result_sourcedid); String setting = (String) content.get(LTIService.LTI_SETTINGS); if ( setting != null ) { setProperty(ltiProps,"ext_ims_lti_tool_setting", setting); } String service_url = ServerConfigurationService.getString("basiclti.consumer.ext_ims_lti_tool_setting_url",null); if ( service_url == null ) service_url = getOurServerUrl() + LTI1_PATH; setProperty(ltiProps,"ext_ims_lti_tool_setting_url", service_url); } else { String settings_url = getOurServerUrl() + LTI2_PATH + SVC_Settings + "/"; setProperty(lti2subst,"LtiLink.custom.url", settings_url + LTI2Util.SCOPE_LtiLink + "/" + resource_link_id); setProperty(lti2subst,"ToolProxyBinding.custom.url", settings_url + LTI2Util.SCOPE_ToolProxyBinding + "/" + resource_link_id); setProperty(lti2subst,"ToolProxy.custom.url", settings_url + LTI2Util.SCOPE_ToolProxy + "/" + key); } } if ( allowroster == 1 ) { setProperty(ltiProps,"ext_ims_lis_memberships_id", result_sourcedid); String roster_url = ServerConfigurationService.getString("basiclti.consumer.ext_ims_lis_memberships_url",null); if ( roster_url == null ) roster_url = getOurServerUrl() + LTI1_PATH; setProperty(ltiProps,"ext_ims_lis_memberships_url", roster_url); } if ( allowlori == 1 ) { setProperty(ltiProps,"ext_lori_api_token", result_sourcedid); setProperty(ltiProps,BasicLTIConstants.LIS_RESULT_SOURCEDID, result_sourcedid); String lori_url = ServerConfigurationService.getString("basiclti.consumer.ext_lori_api_url",null); if ( lori_url == null ) lori_url = getOurServerUrl() + LTI1_PATH; String lori_url_xml = ServerConfigurationService.getString("basiclti.consumer.ext_lori_api_url_xml",null); if ( lori_url_xml == null ) lori_url_xml = getOurServerUrl() + LTI1_PATH; setProperty(ltiProps,"ext_lori_api_url", lori_url); setProperty(ltiProps,"ext_lori_api_url_xml", lori_url_xml); } } // Merge all the sources of properties according to the arcane precedence for launch Properties custom = new Properties(); LTI2Util.mergeLTI2Custom(custom, (String) content.get(LTIService.LTI_SETTINGS)); LTI2Util.mergeLTI2Custom(custom, (String) tool.get(LTIService.LTI_SETTINGS)); LTI2Util.mergeLTI2Parameters(custom, (String) tool.get(LTIService.LTI_PARAMETER)); if ( proxyBinding != null ) { LTI2Util.mergeLTI2Custom(custom, (String) proxyBinding.get(LTIService.LTI_SETTINGS)); } if ( deploy != null ) { LTI2Util.mergeLTI2Custom(custom, (String) deploy.get(LTIService.LTI_SETTINGS)); } int allowCustom = getInt(tool.get(LTIService.LTI_ALLOWCUSTOM)); if ( allowCustom == 1 ) LTI2Util.mergeLTI1Custom(custom, (String) content.get(LTIService.LTI_CUSTOM)); LTI2Util.mergeLTI1Custom(custom, (String) tool.get(LTIService.LTI_CUSTOM)); // System.out.println("ltiProps="+ltiProps); // System.out.println("toolProps="+toolProps); M_log.debug("lti2subst="+lti2subst); M_log.debug("before custom="+custom); LTI2Util.substituteCustom(custom, lti2subst); M_log.debug("after custom="+custom); // Place the custom values into the launch LTI2Util.addCustomToLaunch(ltiProps, custom); return postLaunchHTML(toolProps, ltiProps, rb); } // An LTI 2.0 Registration launch // This must return an HTML message as the [0] in the array // If things are successful - the launch URL is in [1] public static String[] postRegisterHTML(Long deployKey, Map<String,Object> tool, ResourceLoader rb, String placementId) { if ( tool == null ) { return postError("<p>" + getRB(rb, "error.tool.missing" ,"Tool item is missing or improperly configured.")+"</p>" ); } int status = getInt(tool.get(LTIService.LTI_REG_STATE)); if ( status != 0 ) return postError("<p>" + getRB(rb, "error.lti2.badstate" ,"Tool is in the wrong state to register")+"</p>" ); String launch_url = (String) tool.get(LTIService.LTI_REG_LAUNCH); if ( launch_url == null ) return postError("<p>" + getRB(rb, "error.lti2.noreg" ,"This tool is has no registration url.")+"</p>" ); String password = (String) tool.get(LTIService.LTI_REG_PASSWORD); String key = (String) tool.get(LTIService.LTI_REG_KEY); String consumerkey = (String) tool.get(LTIService.LTI_CONSUMERKEY); if ( password == null || key == null || consumerkey == null) { return postError("<p>" + getRB(rb, "error.lti2.partial" ,"Tool item is incomplete, missing a key and password.")+"</p>" ); } // Start building up the properties Properties ltiProps = new Properties(); setProperty(ltiProps, BasicLTIConstants.LTI_VERSION, LTI2Constants.LTI2_VERSION_STRING); setProperty(ltiProps, LTI2Constants.REG_KEY,key); // TODO: Lets show off and encrypt this secret too... setProperty(ltiProps, LTI2Constants.REG_PASSWORD,password); setProperty(ltiProps, BasicLTIUtil.BASICLTI_SUBMIT, getRB(rb, "launch.button", "Press to Launch External Tool")); setProperty(ltiProps, BasicLTIConstants.LTI_MESSAGE_TYPE, BasicLTIConstants.LTI_MESSAGE_TYPE_TOOLPROXYREGISTRATIONREQUEST); String serverUrl = getOurServerUrl(); setProperty(ltiProps, LTI2Constants.TC_PROFILE_URL,serverUrl + LTI2_PATH + SVC_tc_profile + "/" + consumerkey); setProperty(ltiProps, BasicLTIConstants.LAUNCH_PRESENTATION_RETURN_URL, serverUrl + "/portal/tool/"+placementId+"?panel=Activate&id="+deployKey); int debug = getInt(tool.get(LTIService.LTI_DEBUG)); debug = 1; M_log.debug("ltiProps="+ltiProps); boolean dodebug = debug == 1; String postData = BasicLTIUtil.postLaunchHTML(ltiProps, launch_url, dodebug); String [] retval = { postData, launch_url }; return retval; } // An LTI 2.0 ReRegistration launch // This must return an HTML message as the [0] in the array // If things are successful - the launch URL is in [1] public static String[] postReRegisterHTML(Long deployKey, Map<String,Object> deploy, ResourceLoader rb, String placementId) { if ( deploy == null ) { return postError("<p>" + getRB(rb, "error.deploy.missing" ,"Deployment is missing or improperly configured.")+"</p>" ); } int status = getInt(deploy.get("reg_state")); if ( status == 0 ) return postError("<p>" + getRB(rb, "error.deploy.badstate" ,"Deployment is in the wrong state to register")+"</p>" ); String launch_url = (String) deploy.get("reg_launch"); if ( launch_url == null ) return postError("<p>" + getRB(rb, "error.deploy.noreg" ,"This deployment is has no registration url.")+"</p>" ); String consumerkey = (String) deploy.get(LTIService.LTI_CONSUMERKEY); String secret = (String) deploy.get(LTIService.LTI_SECRET); if ( secret == null || consumerkey == null) { return postError("<p>" + getRB(rb, "error.deploy.partial" ,"Deployment is incomplete, missing a key and secret.")+"</p>" ); } // Start building up the properties Properties ltiProps = new Properties(); setProperty(ltiProps, BasicLTIConstants.LTI_VERSION, LTI2Constants.LTI2_VERSION_STRING); setProperty(ltiProps, BasicLTIUtil.BASICLTI_SUBMIT, getRB(rb, "launch.button", "Press to Launch External Tool")); setProperty(ltiProps, BasicLTIConstants.LTI_MESSAGE_TYPE, BasicLTIConstants.LTI_MESSAGE_TYPE_TOOLPROXY_RE_REGISTRATIONREQUEST); String serverUrl = getOurServerUrl(); setProperty(ltiProps, LTI2Constants.TC_PROFILE_URL,serverUrl + LTI2_PATH + SVC_tc_profile + "/" + consumerkey); setProperty(ltiProps, BasicLTIConstants.LAUNCH_PRESENTATION_RETURN_URL, serverUrl + "/portal/tool/"+placementId+"?panel=Activate&id="+deployKey); int debug = getInt(deploy.get(LTIService.LTI_DEBUG)); debug = 1; ltiProps = BasicLTIUtil.signProperties(ltiProps, launch_url, "POST", consumerkey, secret, null, null, null); M_log.debug("signed ltiProps="+ltiProps); boolean dodebug = debug == 1; String postData = BasicLTIUtil.postLaunchHTML(ltiProps, launch_url, dodebug); String [] retval = { postData, launch_url }; return retval; } // This must return an HTML message as the [0] in the array // If things are successful - the launch URL is in [1] public static String[] postLaunchHTML(String placementId, ResourceLoader rb) { if ( placementId == null ) return postError("<p>" + getRB(rb, "error.missing" ,"Error, missing placementId")+"</p>" ); ToolConfiguration placement = SiteService.findTool(placementId); if ( placement == null ) return postError("<p>" + getRB(rb, "error.load" ,"Error, cannot load placement=")+placementId+".</p>"); // Add user, course, etc to the launch parameters Properties ltiProps = new Properties(); if ( ! sakaiInfo(ltiProps, placement, rb) ) { return postError("<p>" + getRB(rb, "error.missing", "Error, cannot load Sakai information for placement=")+placementId+".</p>"); } // Retrieve the launch detail Properties toolProps = new Properties(); if ( ! loadFromPlacement(toolProps, ltiProps, placement) ) { return postError("<p>" + getRB(rb, "error.nolaunch" ,"Not Configured.")+"</p>"); } return postLaunchHTML(toolProps, ltiProps, rb); } public static String[] postLaunchHTML(Properties toolProps, Properties ltiProps, ResourceLoader rb) { String launch_url = toolProps.getProperty("secure_launch_url"); if ( launch_url == null ) launch_url = toolProps.getProperty("launch_url"); if ( launch_url == null ) return postError("<p>" + getRB(rb, "error.missing" ,"Not configured")+"</p>"); String org_guid = ServerConfigurationService.getString("basiclti.consumer_instance_guid",null); String org_desc = ServerConfigurationService.getString("basiclti.consumer_instance_description",null); String org_url = ServerConfigurationService.getString("basiclti.consumer_instance_url",null); // Look up the LMS-wide secret and key - default key is guid String key = getToolConsumerInfo(launch_url,"key"); if ( key == null ) key = org_guid; String secret = getToolConsumerInfo(launch_url,LTIService.LTI_SECRET); // Demand key/secret in a pair if ( key == null || secret == null ) { key = null; secret = null; } // If we do not have LMS-wide info, use the local key/secret if ( secret == null ) { secret = toNull(toolProps.getProperty(LTIService.LTI_SECRET)); key = toNull(toolProps.getProperty("key")); } // If secret is encrypted, decrypt it secret = decryptSecret(secret); // Pull in all of the custom parameters for(Object okey : toolProps.keySet() ) { String skey = (String) okey; if ( ! skey.startsWith(BasicLTIConstants.CUSTOM_PREFIX) ) continue; String value = toolProps.getProperty(skey); if ( value == null ) continue; setProperty(ltiProps, skey, value); } String oauth_callback = ServerConfigurationService.getString("basiclti.oauth_callback",null); // Too bad there is not a better default callback url for OAuth // Actually since we are using signing-only, there is really not much point // In OAuth 6.2.3, this is after the user is authorized if ( oauth_callback == null ) oauth_callback = "about:blank"; setProperty(ltiProps, "oauth_callback", oauth_callback); setProperty(ltiProps, BasicLTIUtil.BASICLTI_SUBMIT, getRB(rb, "launch.button", "Press to Launch External Tool")); // Sanity checks if ( secret == null ) { return postError("<p>" + getRB(rb, "error.nosecret", "Error - must have a secret.")+"</p>"); } if ( secret != null && key == null ){ return postError("<p>" + getRB(rb, "error.nokey", "Error - must have a secret and a key.")+"</p>"); } ltiProps = BasicLTIUtil.signProperties(ltiProps, launch_url, "POST", key, secret, org_guid, org_desc, org_url); if ( ltiProps == null ) return postError("<p>" + getRB(rb, "error.sign", "Error signing message.")+"</p>"); dPrint("LAUNCH III="+ltiProps); String debugProperty = toolProps.getProperty(LTIService.LTI_DEBUG); boolean dodebug = "on".equals(debugProperty) || "1".equals(debugProperty); String postData = BasicLTIUtil.postLaunchHTML(ltiProps, launch_url, dodebug); String [] retval = { postData, launch_url }; return retval; } public static String getSourceDID(User user, Placement placement, Properties config) { String placementSecret = toNull(getCorrectProperty(config,"placementsecret", placement)); if ( placementSecret == null ) return null; return getSourceDID(user, placement.getId(), placementSecret); } public static String getSourceDID(User user, String placeStr, String placementSecret) { if ( placementSecret == null ) return null; String suffix = ":::" + user.getId() + ":::" + placeStr; String base_string = placementSecret + suffix; String signature = ShaUtil.sha256Hash(base_string); return signature + suffix; } public static String[] postError(String str) { String [] retval = { str }; return retval; } public static String getRB(ResourceLoader rb, String key, String def) { if ( rb == null ) return def; return rb.getString(key, def); } // To make absolutely sure we never send an XSS, we clean these values public static void setProperty(Properties props, String key, String value) { if ( value == null ) return; if ( props == null ) return; value = Web.cleanHtml(value); if ( value.trim().length() < 1 ) return; props.setProperty(key, value); } private static String getExternalRealmId(String siteId) { String realmId = SiteService.siteReference(siteId); String rv = null; try { AuthzGroup realm = AuthzGroupService.getAuthzGroup(realmId); rv = realm.getProviderGroupId(); } catch (GroupNotDefinedException e) { dPrint("SiteParticipantHelper.getExternalRealmId: site realm not found"+e.getMessage()); } return rv; } // getExternalRealmId // Look through a series of secrets from the properties based on the launchUrl private static String getToolConsumerInfo(String launchUrl, String data) { String default_secret = ServerConfigurationService.getString("basiclti.consumer_instance_"+data,null); dPrint("launchUrl = "+launchUrl); URL url = null; try { url = new URL(launchUrl); } catch (Exception e) { url = null; } if ( url == null ) return default_secret; String hostName = url.getHost(); dPrint("host = "+hostName); if ( hostName == null || hostName.length() < 1 ) return default_secret; // Look for the property starting with the full name String org_info = ServerConfigurationService.getString("basiclti.consumer_instance_"+data+"."+hostName,null); if ( org_info != null ) return org_info; for ( int i = 0; i < hostName.length(); i++ ) { if ( hostName.charAt(i) != '.' ) continue; if ( i > hostName.length()-2 ) continue; String hostPart = hostName.substring(i+1); String propName = "basiclti.consumer_instance_"+data+"."+hostPart; org_info = ServerConfigurationService.getString(propName,null); if ( org_info != null ) return org_info; } return default_secret; } // expected_oauth_key can be null - if it is non-null it must match the key in the request public static Object validateMessage(HttpServletRequest request, String URL, String oauth_secret, String expected_oauth_key) { oauth_secret = decryptSecret(oauth_secret); return BasicLTIUtil.validateMessage(request, URL, oauth_secret, expected_oauth_key); } // Returns: // String implies error // Boolean.TRUE - Sourcedid checks out // Boolean.FALSE - Sourcedid or secret fail public static Object checkSourceDid(String sourcedid, HttpServletRequest request, LTIService ltiService) { return handleGradebook(sourcedid, request, ltiService, false, false, null, null); } // Grade retrieval Map<String, Object> with "grade" => Double and "comment" => String public static Object getGrade(String sourcedid, HttpServletRequest request, LTIService ltiService) { return handleGradebook(sourcedid, request, ltiService, true, false, null, null); } // Boolean.TRUE - Grade updated public static Object setGrade(String sourcedid, HttpServletRequest request, LTIService ltiService, Double grade, String comment) { return handleGradebook(sourcedid, request, ltiService, false, false, grade, comment); } // Boolean.TRUE - Grade deleted public static Object deleteGrade(String sourcedid, HttpServletRequest request, LTIService ltiService, Double grade, String comment) { return handleGradebook(sourcedid, request, ltiService, false, true, null, null); } // Quite a long bit of code private static Object handleGradebook(String sourcedid, HttpServletRequest request, LTIService ltiService, boolean isRead, boolean isDelete, Double theGrade, String comment) { // Truncate this to the maximum length to insure no cruft at the end if ( sourcedid.length() > 2048) sourcedid = sourcedid.substring(0,2048); // Attempt to parse the sourcedid, any failure is fatal String placement_id = null; String signature = null; String user_id = null; try { int pos = sourcedid.indexOf(":::"); if ( pos > 0 ) { signature = sourcedid.substring(0, pos); String dec2 = sourcedid.substring(pos+3); pos = dec2.indexOf(":::"); user_id = dec2.substring(0,pos); placement_id = dec2.substring(pos+3); } } catch (Exception e) { return "Unable to decrypt result_sourcedid=" + sourcedid; } M_log.debug("signature="+signature); M_log.debug("user_id="+user_id); M_log.debug("placement_id="+placement_id); Properties pitch = getPropertiesFromPlacement(placement_id, ltiService); if ( pitch == null ) { return "Error retrieving result_sourcedid information"; } String siteId = pitch.getProperty(LTIService.LTI_SITE_ID); Site site = null; try { site = SiteService.getSite(siteId); } catch (Exception e) { return "Error retrieving result_sourcedid site: "+e.getLocalizedMessage(); } // Check the message signature using OAuth String oauth_secret = pitch.getProperty(LTIService.LTI_SECRET); M_log.debug("oauth_secret: "+oauth_secret); oauth_secret = decryptSecret(oauth_secret); M_log.debug("oauth_secret (decrypted): "+oauth_secret); String oauth_consumer_key = pitch.getProperty(LTIService.LTI_CONSUMERKEY); M_log.debug("oauth_consumer_key: "+oauth_consumer_key); String URL = getOurServletPath(request); // Validate the incoming message Object retval = validateMessage(request, URL, oauth_secret, oauth_consumer_key); if ( retval instanceof String ) return retval; // Check the signature of the sourcedid to make sure it was not altered String placement_secret = pitch.getProperty(LTIService.LTI_PLACEMENTSECRET); if ( placement_secret == null ) { return "Could not find placement secret"; } String pre_hash = placement_secret + ":::" + user_id + ":::" + placement_id; String received_signature = ShaUtil.sha256Hash(pre_hash); M_log.debug("Received signature="+signature+" received="+received_signature); boolean matched = signature.equals(received_signature); String old_placement_secret = pitch.getProperty(LTIService.LTI_OLDPLACEMENTSECRET); if ( old_placement_secret != null && ! matched ) { pre_hash = placement_secret + ":::" + user_id + ":::" + placement_id; received_signature = ShaUtil.sha256Hash(pre_hash); M_log.debug("Received signature II="+signature+" received="+received_signature); matched = signature.equals(received_signature); } if ( !matched ) return "Sourcedid signature did not match"; // If we are not supposed to lookup or set the grade, we are done if ( isRead == false && isDelete == false && theGrade == null ) return new Boolean(matched); // Look up the assignment so we can find the max points GradebookService g = (GradebookService) ComponentManager .get("org.sakaiproject.service.gradebook.GradebookService"); // Make sure the user exists in the site boolean userExistsInSite = false; try { Member member = site.getMember(user_id); if(member != null ) userExistsInSite = true; } catch (Exception e) { M_log.warn(e.getLocalizedMessage() + " siteId="+siteId, e); return "User not found in site"; } // Make sure the placement is configured to receive grades String assignment = pitch.getProperty("assignment"); M_log.debug("ASSN="+assignment); if ( assignment == null ) { return "Assignment not set in placement"; } Assignment assignmentObject = null; pushAdvisor(); try { List gradebookAssignments = g.getAssignments(siteId); for (Iterator i=gradebookAssignments.iterator(); i.hasNext();) { Assignment gAssignment = (Assignment) i.next(); if ( gAssignment.isExternallyMaintained() ) continue; if ( assignment.equals(gAssignment.getName()) ) { assignmentObject = gAssignment; break; } } } catch (Exception e) { assignmentObject = null; // Just to make double sure } // Attempt to add assignment to grade book if ( assignmentObject == null && g.isGradebookDefined(siteId) ) { try { assignmentObject = new Assignment(); assignmentObject.setPoints(Double.valueOf(100)); assignmentObject.setExternallyMaintained(false); assignmentObject.setName(assignment); assignmentObject.setReleased(true); assignmentObject.setUngraded(false); g.addAssignment(siteId, assignmentObject); M_log.info("Added assignment: "+assignment); } catch (ConflictingAssignmentNameException e) { M_log.warn("ConflictingAssignmentNameException while adding assignment" + e.getMessage()); assignmentObject = null; // Just to make sure } catch (Exception e) { M_log.warn("GradebookNotFoundException (may be because GradeBook has not yet been added to the Site) " + e.getMessage()); assignmentObject = null; // Just to make double sure } } // Now read, set, or delete the grade... Session sess = SessionManager.getCurrentSession(); String message = null; try { // Indicate "who" is setting this grade - needs to be a real user account String gb_user_id = ServerConfigurationService.getString( "basiclti.outcomes.userid", "admin"); String gb_user_eid = ServerConfigurationService.getString( "basiclti.outcomes.usereid", gb_user_id); sess.setUserId(gb_user_id); sess.setUserEid(gb_user_eid); if ( isRead ) { String actualGrade = g.getAssignmentScoreString(siteId, assignment, user_id); Double dGrade = null; if ( actualGrade != null && actualGrade.length() > 0 ) { dGrade = new Double(actualGrade); dGrade = dGrade / assignmentObject.getPoints(); } CommentDefinition commentDef = g.getAssignmentScoreComment(siteId, assignment, user_id); message = "Result read"; Map<String, Object> retMap = new TreeMap<String, Object> (); retMap.put("grade",dGrade); retMap.put("comment",commentDef.getCommentText()); retval = retMap; } else if ( isDelete ) { g.setAssignmentScore(siteId, assignment, user_id, null, "External Outcome"); M_log.info("Delete Score site=" + siteId + " assignment="+ assignment + " user_id=" + user_id); message = "Result deleted"; retval = Boolean.TRUE; } else { if ( theGrade < 0.0 || theGrade > 1.0 ) { throw new Exception("Grade out of range"); } theGrade = theGrade * assignmentObject.getPoints(); g.setAssignmentScore(siteId, assignment, user_id, theGrade, "External Outcome"); g.setAssignmentScoreComment(siteId, assignment, user_id, comment); M_log.info("Stored Score=" + siteId + " assignment="+ assignment + " user_id=" + user_id + " score="+ theGrade); message = "Result replaced"; retval = Boolean.TRUE; } } catch (Exception e) { retval = "Grade failure "+e.getMessage()+" siteId="+siteId; } finally { sess.invalidate(); // Make sure to leave no traces popAdvisor(); } return retval; } // Extract the necessary properties from a placement public static Properties getPropertiesFromPlacement(String placement_id, LTIService ltiService) { // These are the fields from a placement - they are not an exact match // for the fields in tool/content String [] fieldList = { "key", LTIService.LTI_SECRET, LTIService.LTI_PLACEMENTSECRET, LTIService.LTI_OLDPLACEMENTSECRET, LTIService.LTI_ALLOWSETTINGS, "assignment", LTIService.LTI_ALLOWROSTER, "releasename", "releaseemail", "toolsetting", "allowlori"}; Properties retval = new Properties(); String siteId = null; if ( isPlacement(placement_id) ) { ToolConfiguration placement = null; Properties config = null; try { placement = SiteService.findTool(placement_id); config = placement.getConfig(); siteId = placement.getSiteId(); } catch (Exception e) { M_log.debug("Error getPropertiesFromPlacement: "+e.getLocalizedMessage(), e); return null; } retval.setProperty("placementId",placement_id); retval.setProperty(LTIService.LTI_SITE_ID,siteId); for ( String field : fieldList ) { String value = toNull(getCorrectProperty(config,field, placement)); if ( field.equals("toolsetting") ) { value = config.getProperty("toolsetting", null); field = LTIService.LTI_SETTINGS; } if ( value == null ) continue; if ( field.equals("releasename") ) field = LTIService.LTI_SENDNAME; if ( field.equals("releaseemail") ) field = LTIService.LTI_SENDEMAILADDR; if ( field.equals("key") ) field = LTIService.LTI_CONSUMERKEY; retval.setProperty(field, value); } } else { // Get information from content item Map<String,Object> content = null; Map<String,Object> tool = null; String contentStr = placement_id.substring(8); Long contentKey = getLongKey(contentStr); if ( contentKey < 0 ) return null; // Leave off the siteId - bypass all checking - because we need to // finde the siteId from the content item content = ltiService.getContentDao(contentKey); if ( content == null ) return null; siteId = (String) content.get(LTIService.LTI_SITE_ID); if ( siteId == null ) return null; retval.setProperty("contentKey",contentStr); retval.setProperty(LTIService.LTI_SITE_ID,siteId); Long toolKey = getLongKey(content.get(LTIService.LTI_TOOL_ID)); if ( toolKey < 0 ) return null; tool = ltiService.getToolDao(toolKey, siteId); if ( tool == null ) return null; // Adjust the content items based on the tool items if ( tool != null || content != null ) { ltiService.filterContent(content, tool); } for (String formInput : LTIService.TOOL_MODEL) { Properties info = parseFormString(formInput); String field = info.getProperty("field", null); String type = info.getProperty("type", null); Object o = tool.get(field); if ( o instanceof String ) { retval.setProperty(field,(String) o); continue; } if ( "checkbox".equals(type) ) { int check = getInt(o); if ( check == 1 ) { retval.setProperty(field,"on"); } else { retval.setProperty(field,"off"); } } } for (String formInput : LTIService.CONTENT_MODEL) { Properties info = parseFormString(formInput); String field = info.getProperty("field", null); String type = info.getProperty("type", null); Object o = content.get(field); if ( o instanceof String ) { retval.setProperty(field,(String) o); continue; } if ( "checkbox".equals(type) ) { int check = getInt(o); if ( check == 1 ) { retval.setProperty(field,"on"); } else { retval.setProperty(field,"off"); } } } retval.setProperty("assignment",(String)content.get("title")); } return retval; } public static boolean isPlacement(String placement_id) { if ( placement_id == null ) return false; return ! (placement_id.startsWith("content:") && placement_id.length() > 8) ; } // Since ServerConfigurationService.getServerUrl() is wonky because it sometimes looks // at request.getServerName() instead of the serverUrl property we have our own // priority to determine our current url. // BLTI-273 public static String getOurServerUrl() { String ourUrl = ServerConfigurationService.getString("sakai.lti.serverUrl"); if (ourUrl == null || ourUrl.equals("")) ourUrl = ServerConfigurationService.getString("serverUrl"); if (ourUrl == null || ourUrl.equals("")) ourUrl = ServerConfigurationService.getServerUrl(); if (ourUrl == null || ourUrl.equals("")) ourUrl = "http://127.0.0.1:8080"; if ( ourUrl.endsWith("/") && ourUrl.length() > 2 ) ourUrl = ourUrl.substring(0,ourUrl.length()-1); return ourUrl; } public static String getOurServletPath(HttpServletRequest request) { String URLstr = request.getRequestURL().toString(); String retval = URLstr.replaceFirst("^https??://[^/]*",getOurServerUrl()); return retval; } public static String toNull(String str) { if ( str == null ) return null; if ( str.trim().length() < 1 ) return null; return str; } // Pull in a few things to avoid circular dependency public static int getInt(Object o) { if ( o instanceof String ) { try { return (new Integer((String) o)).intValue(); } catch (Exception e) { return -1; } } if ( o instanceof Number ) return ( (Number) o).intValue(); return -1; } public static String[] positional = { "field", "type" }; public static Properties parseFormString(String str) { Properties op = new Properties(); String[] pairs = str.split(":"); int i = 0; for (String s : pairs) { String[] kv = s.split("="); if (kv.length == 2) { op.setProperty(kv[0], kv[1]); } else if (kv.length == 1 && i < positional.length) { op.setProperty(positional[i++], kv[0]); } else { // TODO : Log something here } } return op; } public static Long getLongKey(Object key) { return getLong(key); } public static Long getLong(Object key) { Long retval = getLongNull(key); if (retval != null) return retval; return new Long(-1); } public static Long getLongNull(Object key) { if (key == null) return null; if (key instanceof Number) return new Long(((Number) key).longValue()); if (key instanceof String) { try { return new Long((String) key); } catch (Exception e) { return null; } } return null; } /** * Setup a security advisor. */ public static 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 static void popAdvisor() { SecurityService.popAdvisor(); } }