/** * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-blis/src/java/org/sakaiproject/lti2/LTI2Service.java $ * $Id: LTI2Service.java 131954 2013-11-26 16:04:17Z csev@umich.edu $ * * 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.lti2; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.HashSet; import java.util.Iterator; import java.util.Collections; import java.util.ArrayList; import java.util.Map; import java.util.TreeMap; import java.util.Properties; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletContext; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.oauth.OAuthAccessor; import net.oauth.OAuthConsumer; import net.oauth.OAuthMessage; import net.oauth.OAuthValidator; import net.oauth.SimpleOAuthValidator; import net.oauth.server.OAuthServlet; import net.oauth.signature.OAuthSignatureMethod; import org.json.simple.JSONValue; import org.json.simple.JSONObject; import org.json.simple.JSONArray; import org.sakaiproject.component.cover.ComponentManager; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.imsglobal.basiclti.BasicLTIUtil; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.basiclti.util.SakaiBLTIUtil; import org.imsglobal.basiclti.BasicLTIConstants; import org.imsglobal.lti2.LTI2Constants; import org.imsglobal.lti2.LTI2Config; import org.imsglobal.lti2.LTI2Util; import org.imsglobal.lti2.objects.*; import org.sakaiproject.lti2.SakaiLTI2Services; import org.sakaiproject.lti2.SakaiLTI2Config; import org.sakaiproject.lti2.SakaiLTI2Base; import org.imsglobal.json.IMSJSONRequest; import org.sakaiproject.lti.api.LTIService; import org.sakaiproject.util.foorm.SakaiFoorm; import org.sakaiproject.util.foorm.FoormUtil; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.ObjectWriter; /** * Notes: * * This program is directly exposed as a URL to receive IMS Basic LTI messages * so it must be carefully reviewed and any changes must be looked at carefully. * */ @SuppressWarnings("deprecation") public class LTI2Service extends HttpServlet { private static final long serialVersionUID = 1L; private static Log M_log = LogFactory.getLog(LTI2Service.class); private static ResourceLoader rb = new ResourceLoader("blis"); protected static SakaiFoorm foorm = new SakaiFoorm(); protected static LTIService ltiService = null; protected String resourceUrl = null; protected Service_offered LTI2ResultItem = null; protected Service_offered LTI2LtiLinkSettings = null; protected Service_offered LTI2ToolProxyBindingSettings = null; protected Service_offered LTI2ToolProxySettings = null; // Copy these in... private static final String SVC_tc_profile = SakaiBLTIUtil.SVC_tc_profile; private static final String SVC_tc_registration = SakaiBLTIUtil.SVC_tc_registration; private static final String SVC_Settings = SakaiBLTIUtil.SVC_Settings; private static final String SVC_Result = SakaiBLTIUtil.SVC_Result; private static final String LTI1_PATH = SakaiBLTIUtil.LTI1_PATH; private static final String LTI2_PATH = SakaiBLTIUtil.LTI2_PATH; private static final String APPLICATION_JSON = "application/json"; @Override public void init(ServletConfig config) throws ServletException { super.init(config); if ( ltiService == null ) ltiService = (LTIService) ComponentManager.get("org.sakaiproject.lti.api.LTIService"); resourceUrl = SakaiBLTIUtil.getOurServerUrl() + LTI2_PATH; LTI2ResultItem = StandardServices.LTI2ResultItem(resourceUrl + SVC_Result + "/{" + BasicLTIConstants.LIS_RESULT_SOURCEDID + "}"); LTI2LtiLinkSettings = StandardServices.LTI2LtiLinkSettings(resourceUrl + SVC_Settings + "/" + LTI2Util.SCOPE_LtiLink + "/{" + BasicLTIConstants.RESOURCE_LINK_ID + "}"); LTI2ToolProxyBindingSettings = StandardServices.LTI2ToolProxySettings(resourceUrl + SVC_Settings + "/" + LTI2Util.SCOPE_ToolProxyBinding + "/{" + BasicLTIConstants.RESOURCE_LINK_ID + "}"); LTI2ToolProxySettings = StandardServices.LTI2ToolProxySettings(resourceUrl + SVC_Settings + "/" + LTI2Util.SCOPE_ToolProxy + "/{" + LTI2Constants.TOOL_PROXY_GUID + "}"); } protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request,response); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request,response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { doRequest(request, response); } catch (Exception e) { String ipAddress = request.getRemoteAddr(); String uri = request.getRequestURI(); M_log.warn("General LTI2 Failure URI="+uri+" IP=" + ipAddress); e.printStackTrace(); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); doErrorJSON(request, response, null, "General failure", e); } } @SuppressWarnings("unchecked") protected void doRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String ipAddress = request.getRemoteAddr(); M_log.debug("LTI Service request from IP=" + ipAddress); String rpi = request.getPathInfo(); String uri = request.getRequestURI(); String [] parts = uri.split("/"); if ( parts.length < 4 ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, null, "Incorrect url format", null); return; } String controller = parts[3]; if ( SVC_tc_profile.equals(controller) && parts.length == 5 ) { String profile_id = parts[4]; getToolConsumerProfile(request,response,profile_id); return; } else if ( SVC_tc_registration.equals(controller) && parts.length == 5 ) { String profile_id = parts[4]; registerToolProviderProfile(request, response, profile_id); return; } else if ( SVC_Result.equals(controller) && parts.length == 5 ) { String sourcedid = parts[4]; handleResultRequest(request, response, sourcedid); return; } else if ( SVC_Settings.equals(controller) && parts.length >= 6 ) { handleSettingsRequest(request, response, parts); return; } IMSJSONRequest jsonRequest = new IMSJSONRequest(request); if ( jsonRequest.valid ) { System.out.println(jsonRequest.getPostBody()); } response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); M_log.warn("Unknown request="+uri); doErrorJSON(request, response, null, "Unknown request="+uri, null); } protected void getToolConsumerProfile(HttpServletRequest request, HttpServletResponse response,String profile_id) { Map<String,Object> deploy = ltiService.getDeployForConsumerKeyDao(profile_id); if ( deploy == null ) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } ToolConsumer consumer = getToolConsumerProfile(deploy, profile_id); ObjectMapper mapper = new ObjectMapper(); try { // http://stackoverflow.com/questions/6176881/how-do-i-make-jackson-pretty-print-the-json-content-it-generates ObjectWriter writer = mapper.defaultPrettyPrintingWriter(); // ***IMPORTANT!!!*** for Jackson 2.x use the line below instead of the one above: // ObjectWriter writer = mapper.writer().withDefaultPrettyPrinter(); // System.out.println(mapper.writeValueAsString(consumer)); response.setContentType(APPLICATION_JSON); PrintWriter out = response.getWriter(); out.println(writer.writeValueAsString(consumer)); // System.out.println(writer.writeValueAsString(consumer)); } catch (Exception e) { e.printStackTrace(); } } protected ToolConsumer getToolConsumerProfile(Map<String, Object> deploy, String profile_id) { // Load the configuration data LTI2Config cnf = new SakaiLTI2Config(); if ( cnf.getGuid() == null ) { M_log.error("*********************************************"); M_log.error("* LTI2 NOT CONFIGURED - Using Sample Data *"); M_log.error("* Do not use this in production. Test only *"); M_log.error("*********************************************"); // cnf = new org.imsglobal.lti2.LTI2ConfigSample(); cnf = new SakaiLTI2Base(); } String serverUrl = SakaiBLTIUtil.getOurServerUrl(); ToolConsumer consumer = new ToolConsumer(profile_id+"", resourceUrl, cnf); List<String> capabilities = consumer.getCapability_offered(); if (foorm.getLong(deploy.get(LTIService.LTI_SENDEMAILADDR)) > 0 ) { LTI2Util.allowEmail(capabilities); } if (foorm.getLong(deploy.get(LTIService.LTI_SENDNAME)) > 0 ) { LTI2Util.allowName(capabilities); } List<Service_offered> services = consumer.getService_offered(); services.add(StandardServices.LTI2Registration(serverUrl + LTI2_PATH + SVC_tc_registration + "/" + profile_id)); String allowOutcomes = ServerConfigurationService.getString(SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED, SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED_DEFAULT); if ("true".equals(allowOutcomes) && foorm.getLong(deploy.get(LTIService.LTI_ALLOWOUTCOMES)) > 0 ) { LTI2Util.allowResult(capabilities); services.add(LTI2ResultItem); services.add(StandardServices.LTI1Outcomes(serverUrl+LTI1_PATH)); services.add(SakaiLTI2Services.BasicOutcomes(serverUrl+LTI1_PATH)); } String allowRoster = ServerConfigurationService.getString(SakaiBLTIUtil.BASICLTI_ROSTER_ENABLED, SakaiBLTIUtil.BASICLTI_ROSTER_ENABLED_DEFAULT); if ("true".equals(allowRoster) && foorm.getLong(deploy.get(LTIService.LTI_ALLOWROSTER)) > 0 ) { services.add(SakaiLTI2Services.BasicRoster(serverUrl+LTI1_PATH)); } String allowSettings = ServerConfigurationService.getString(SakaiBLTIUtil.BASICLTI_SETTINGS_ENABLED, SakaiBLTIUtil.BASICLTI_SETTINGS_ENABLED_DEFAULT); if ("true".equals(allowSettings) && foorm.getLong(deploy.get(LTIService.LTI_ALLOWSETTINGS)) > 0 ) { LTI2Util.allowSettings(capabilities); services.add(SakaiLTI2Services.BasicSettings(serverUrl+LTI1_PATH)); services.add(LTI2LtiLinkSettings); services.add(LTI2ToolProxySettings); services.add(LTI2ToolProxyBindingSettings); } String allowLori = ServerConfigurationService.getString(SakaiBLTIUtil.BASICLTI_LORI_ENABLED, SakaiBLTIUtil.BASICLTI_LORI_ENABLED_DEFAULT); if ("true".equals(allowLori) && foorm.getLong(deploy.get(LTIService.LTI_ALLOWLORI)) > 0 ) { services.add(SakaiLTI2Services.LORI_XML(serverUrl+LTI1_PATH)); } return consumer; } public void registerToolProviderProfile(HttpServletRequest request,HttpServletResponse response, String profile_id) throws java.io.IOException { Map<String,Object> deploy = ltiService.getDeployForConsumerKeyDao(profile_id); if ( deploy == null ) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } Long deployKey = foorm.getLong(deploy.get(LTIService.LTI_ID)); // See if we can even register... Long reg_state = foorm.getLong(deploy.get(LTIService.LTI_REG_STATE)); String key = null; String secret = null; if ( reg_state == 0 ) { key = (String) deploy.get(LTIService.LTI_REG_KEY); secret = (String) deploy.get(LTIService.LTI_REG_PASSWORD); } else { key = (String) deploy.get(LTIService.LTI_CONSUMERKEY); secret = (String) deploy.get(LTIService.LTI_SECRET); } IMSJSONRequest jsonRequest = new IMSJSONRequest(request); if ( ! jsonRequest.valid ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, jsonRequest, "Request is not in a valid format", null); return; } // System.out.println(jsonRequest.getPostBody()); // Lets check the signature if ( key == null || secret == null ) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); doErrorJSON(request, response, jsonRequest, "Deployment is missing credentials", null); return; } jsonRequest.validateRequest(key, secret, request); if ( !jsonRequest.valid ) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); doErrorJSON(request, response, jsonRequest, "OAuth signature failure", null); return; } JSONObject providerProfile = (JSONObject) JSONValue.parse(jsonRequest.getPostBody()); // System.out.println("OBJ:"+providerProfile); if ( providerProfile == null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, jsonRequest, "JSON parse failed", null); return; } JSONObject default_custom = (JSONObject) providerProfile.get(LTI2Constants.CUSTOM); JSONObject security_contract = (JSONObject) providerProfile.get(LTI2Constants.SECURITY_CONTRACT); if ( security_contract == null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, jsonRequest, "JSON missing security_contract", null); return; } String shared_secret = (String) security_contract.get(LTI2Constants.SHARED_SECRET); if ( shared_secret == null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, jsonRequest, "JSON missing shared_secret", null); return; } // Blank out the new shared secret security_contract.put(LTI2Constants.SHARED_SECRET, "*********"); // Make sure that the requested services are a subset of the offered services ToolConsumer consumer = getToolConsumerProfile(deploy, profile_id); JSONArray tool_services = (JSONArray) security_contract.get(LTI2Constants.TOOL_SERVICE); String retval = LTI2Util.validateServices(consumer, providerProfile); if ( retval != null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, jsonRequest, retval, null); return; } // Parse the tool profile bit and extract the tools with error checking retval = LTI2Util.validateCapabilities(consumer, providerProfile); if ( retval != null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, jsonRequest, retval, null); return; } // Passed all the tests, lets commit this... Map<String, Object> deployUpdate = new TreeMap<String, Object> (); shared_secret = SakaiBLTIUtil.encryptSecret(shared_secret); deployUpdate.put(LTIService.LTI_SECRET, shared_secret); // Indicate ready to validate and kill the interim info deployUpdate.put(LTIService.LTI_REG_STATE, LTIService.LTI_REG_STATE_REGISTERED); deployUpdate.put(LTIService.LTI_REG_KEY, ""); deployUpdate.put(LTIService.LTI_REG_PASSWORD, ""); if ( default_custom != null ) deployUpdate.put(LTIService.LTI_SETTINGS, default_custom.toString()); deployUpdate.put(LTIService.LTI_REG_PROFILE, providerProfile.toString()); M_log.debug("deployUpdate="+deployUpdate); Object obj = ltiService.updateDeployDao(deployKey, deployUpdate); boolean success = ( obj instanceof Boolean ) && ( (Boolean) obj == Boolean.TRUE); if ( ! success ) { M_log.warn("updateDeployDao fail deployKey="+deployKey+"\nretval="+obj+"\ndata="+deployUpdate); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, jsonRequest, "Failed update of deployment="+deployKey, null); return; } // Share our happiness with the Tool Provider Map jsonResponse = new TreeMap(); jsonResponse.put(LTI2Constants.CONTEXT,StandardServices.TOOLPROXY_ID_CONTEXT); jsonResponse.put(LTI2Constants.TYPE, StandardServices.TOOLPROXY_ID_TYPE); String serverUrl = ServerConfigurationService.getServerUrl(); jsonResponse.put(LTI2Constants.JSONLD_ID, resourceUrl + SVC_tc_registration + "/" +profile_id); jsonResponse.put(LTI2Constants.TOOL_PROXY_GUID, profile_id); jsonResponse.put(LTI2Constants.CUSTOM_URL, resourceUrl + SVC_Settings + "/" + LTI2Util.SCOPE_ToolProxy + "/" +profile_id); response.setContentType(StandardServices.TOOLPROXY_ID_FORMAT); response.setStatus(HttpServletResponse.SC_CREATED); String jsonText = JSONValue.toJSONString(jsonResponse); M_log.debug(jsonText); PrintWriter out = response.getWriter(); out.println(jsonText); } public void handleResultRequest(HttpServletRequest request,HttpServletResponse response, String sourcedid) throws java.io.IOException { String allowOutcomes = ServerConfigurationService.getString(SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED, SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED_DEFAULT); if ( ! "true".equals(allowOutcomes) ) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); doErrorJSON(request,response, null, "Result resources not available", null); return; } Object retval = null; IMSJSONRequest jsonRequest = null; if ( "GET".equals(request.getMethod()) ) { retval = SakaiBLTIUtil.getGrade(sourcedid, request, ltiService); if ( ! (retval instanceof Map) ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, (String) retval, null); return; } Map grade = (Map) retval; Map jsonResponse = new TreeMap(); Map resultScore = new TreeMap(); jsonResponse.put(LTI2Constants.CONTEXT,StandardServices.RESULT_CONTEXT); jsonResponse.put(LTI2Constants.TYPE, StandardServices.RESULT_TYPE); jsonResponse.put(LTI2Constants.COMMENT, grade.get(LTI2Constants.COMMENT)); resultScore.put(LTI2Constants.TYPE, LTI2Constants.GRADE_TYPE_DECIMAL); resultScore.put(LTI2Constants.VALUE, grade.get(LTI2Constants.GRADE)); jsonResponse.put(LTI2Constants.RESULTSCORE,resultScore); response.setContentType(StandardServices.RESULT_FORMAT); response.setStatus(HttpServletResponse.SC_OK); String jsonText = JSONValue.toJSONString(jsonResponse); M_log.debug(jsonText); PrintWriter out = response.getWriter(); out.println(jsonText); } else if ( "PUT".equals(request.getMethod()) ) { retval = "Error parsing input data"; try { jsonRequest = new IMSJSONRequest(request); // System.out.println(jsonRequest.getPostBody()); JSONObject requestData = (JSONObject) JSONValue.parse(jsonRequest.getPostBody()); String comment = (String) requestData.get(LTI2Constants.COMMENT); JSONObject resultScore = (JSONObject) requestData.get(LTI2Constants.RESULTSCORE); String sGrade = (String) resultScore.get(LTI2Constants.VALUE); Double dGrade = new Double(sGrade); retval = SakaiBLTIUtil.setGrade(sourcedid, request, ltiService, dGrade, comment); } catch (Exception e) { retval = "Error: "+ e.getMessage(); } if ( retval instanceof Boolean && (Boolean) retval ) { response.setStatus(HttpServletResponse.SC_OK); } else { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } } else { retval = "Unsupported operation:" + request.getMethod(); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } if ( retval instanceof String ) { doErrorJSON(request,response, jsonRequest, (String) retval, null); return; } } // If this code looks like a hack - it is because the spec is a hack. // There are five possible scenarios for GET and two possible scenarios // for PUT. I begged to simplify the business logic but was overrulled. // So we write obtuse code. public void handleSettingsRequest(HttpServletRequest request,HttpServletResponse response, String[] parts) throws java.io.IOException { String allowSettings = ServerConfigurationService.getString(SakaiBLTIUtil.BASICLTI_SETTINGS_ENABLED, SakaiBLTIUtil.BASICLTI_SETTINGS_ENABLED_DEFAULT); if ( ! "true".equals(allowSettings) ) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); doErrorJSON(request,response, null, "Tool settings not available", null); return; } String URL = SakaiBLTIUtil.getOurServletPath(request); String scope = parts[4]; // Check to see if we are doing the bubble String bubbleStr = request.getParameter("bubble"); String acceptHdr = request.getHeader("Accept"); String contentHdr = request.getContentType(); System.out.println("accept="+acceptHdr+" bubble="+bubbleStr); if ( bubbleStr != null && bubbleStr.equals("all") && acceptHdr.indexOf(StandardServices.TOOLSETTINGS_FORMAT) < 0 ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request, response, null, "Simple format does not allow bubble=all", null); return; } boolean bubble = bubbleStr != null && "GET".equals(request.getMethod()); boolean distinct = bubbleStr != null && "distinct".equals(bubbleStr) && "GET".equals(request.getMethod()); boolean bubbleAll = bubbleStr != null && "all".equals(bubbleStr) && "GET".equals(request.getMethod()); // Check our input and output formats boolean acceptSimple = acceptHdr == null || acceptHdr.indexOf(StandardServices.TOOLSETTINGS_SIMPLE_FORMAT) >= 0 ; boolean acceptComplex = acceptHdr == null || acceptHdr.indexOf(StandardServices.TOOLSETTINGS_FORMAT) >= 0 ; boolean inputSimple = contentHdr == null || contentHdr.indexOf(StandardServices.TOOLSETTINGS_SIMPLE_FORMAT) >= 0 ; boolean inputComplex = contentHdr != null && contentHdr.indexOf(StandardServices.TOOLSETTINGS_FORMAT) >= 0 ; System.out.println("as="+acceptSimple+" ac="+acceptComplex+" is="+inputSimple+" ic="+inputComplex); // Check the JSON on PUT and check the oauth_body_hash IMSJSONRequest jsonRequest = null; JSONObject requestData = null; if ( "PUT".equals(request.getMethod()) ) { try { jsonRequest = new IMSJSONRequest(request); requestData = (JSONObject) JSONValue.parse(jsonRequest.getPostBody()); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, "Could not parse JSON", e); return; } } String consumer_key = null; String siteId = null; String placement_id = null; Map<String,Object> content = null; Long contentKey = null; Map<String,Object> tool = null; Long toolKey = null; Map<String,Object> proxyBinding = null; Long proxyBindingKey = null; Map<String,Object> deploy = null; Long deployKey = null; if ( LTI2Util.SCOPE_LtiLink.equals(scope) || LTI2Util.SCOPE_ToolProxyBinding.equals(scope) ) { placement_id = parts[5]; System.out.println("placement_id="+placement_id); String contentStr = placement_id.substring(8); contentKey = SakaiBLTIUtil.getLongKey(contentStr); if ( contentKey >= 0 ) { // Leave off the siteId - bypass all checking - because we need to // find the siteId from the content item content = ltiService.getContentDao(contentKey); if ( content != null ) siteId = (String) content.get(LTIService.LTI_SITE_ID); } if ( content == null || siteId == null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, "Bad content item", null); return; } toolKey = SakaiBLTIUtil.getLongKey(content.get(LTIService.LTI_TOOL_ID)); if ( toolKey >= 0 ) { tool = ltiService.getToolDao(toolKey, siteId); } if ( tool == null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, "Bad tool item", null); return; } // Adjust the content items based on the tool items ltiService.filterContent(content, tool); // Check settings to see if we are allowed to do this if (foorm.getLong(content.get(LTIService.LTI_ALLOWOUTCOMES)) > 0 || foorm.getLong(tool.get(LTIService.LTI_ALLOWOUTCOMES)) > 0 ) { // Good news } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); doErrorJSON(request,response, jsonRequest, "Item does not allow tool settings", null); return; } } if ( LTI2Util.SCOPE_ToolProxyBinding.equals(scope) || LTI2Util.SCOPE_LtiLink.equals(scope) ) { proxyBinding = ltiService.getProxyBindingDao(toolKey,siteId); if ( proxyBinding != null ) { proxyBindingKey = SakaiBLTIUtil.getLongKey(proxyBinding.get(LTIService.LTI_ID)); } } // Retrieve the deployment if needed if ( LTI2Util.SCOPE_ToolProxy.equals(scope) ) { consumer_key = parts[5]; deploy = ltiService.getDeployForConsumerKeyDao(consumer_key); if ( deploy == null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, "Bad deploy item", null); return; } deployKey = SakaiBLTIUtil.getLongKey(deploy.get(LTIService.LTI_ID)); } else if ( bubble ) { deployKey = SakaiBLTIUtil.getLongKey(tool.get(LTIService.LTI_DEPLOYMENT_ID)); if ( deployKey >= 0 ) { deploy = ltiService.getDeployDao(deployKey); } if ( deploy == null ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, "Bad deploy item", null); return; } consumer_key = (String) deploy.get(LTIService.LTI_CONSUMERKEY); } // Check settings to see if we are allowed to do this if ( deploy != null ) { if (foorm.getLong(deploy.get(LTIService.LTI_ALLOWOUTCOMES)) > 0 ) { // Good news } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); doErrorJSON(request,response, jsonRequest, "Deployment does not allow tool settings", null); return; } } // The URLs for the various settings resources String settingsUrl = SakaiBLTIUtil.getOurServerUrl() + LTI2_PATH + SVC_Settings; String proxy_url = settingsUrl + "/" + LTI2Util.SCOPE_ToolProxy + "/" + consumer_key; String binding_url = settingsUrl + "/" + LTI2Util.SCOPE_ToolProxyBinding + "/" + placement_id; String link_url = settingsUrl + "/" + LTI2Util.SCOPE_LtiLink + "/" + placement_id; // Load and parse the old settings... JSONObject link_settings = new JSONObject (); JSONObject binding_settings = new JSONObject (); JSONObject proxy_settings = new JSONObject(); if ( content != null ) { link_settings = LTI2Util.parseSettings((String) content.get(LTIService.LTI_SETTINGS)); } if ( proxyBinding != null ) { binding_settings = LTI2Util.parseSettings((String) proxyBinding.get(LTIService.LTI_SETTINGS)); } if ( deploy != null ) { proxy_settings = LTI2Util.parseSettings((String) deploy.get(LTIService.LTI_SETTINGS)); } /* if ( distinct && link_settings != null && scope.equals(LTI2Util.SCOPE_LtiLink) ) { Iterator i = link_settings.keySet().iterator(); while ( i.hasNext() ) { String key = (String) i.next(); if ( binding_settings != null ) binding_settings.remove(key); if ( proxy_settings != null ) proxy_settings.remove(key); } } if ( distinct && binding_settings != null && scope.equals(LTI2Util.SCOPE_ToolProxyBinding) ) { Iterator i = binding_settings.keySet().iterator(); while ( i.hasNext() ) { String key = (String) i.next(); if ( proxy_settings != null ) proxy_settings.remove(key); } } */ // Get the secret for the request... String oauth_secret = null; if ( LTI2Util.SCOPE_LtiLink.equals(scope) ) { oauth_secret = (String) content.get(LTIService.LTI_SECRET); if ( oauth_secret == null || oauth_secret.length() < 1 ) { oauth_secret = (String) tool.get(LTIService.LTI_SECRET); } } else if ( LTI2Util.SCOPE_ToolProxyBinding.equals(scope) ) { oauth_secret = (String) tool.get(LTIService.LTI_SECRET); } else if ( LTI2Util.SCOPE_ToolProxy.equals(scope) ) { oauth_secret = (String) deploy.get(LTIService.LTI_SECRET); } else { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, "Bad Setttings Scope="+scope, null); return; } // Validate the incoming message Object retval = SakaiBLTIUtil.validateMessage(request, URL, oauth_secret, consumer_key); if ( retval instanceof String ) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); doErrorJSON(request,response, jsonRequest, (String) retval, null); return; } // For a GET request we depend on LTI2Util to do the GET logic if ( "GET".equals(request.getMethod()) ) { Object obj = LTI2Util.getSettings(request, scope, link_settings, binding_settings, proxy_settings, link_url, binding_url, proxy_url); if ( obj instanceof String ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, (String) obj, null); return; } if ( acceptComplex ) { response.setContentType(StandardServices.TOOLSETTINGS_FORMAT); } else { response.setContentType(StandardServices.TOOLSETTINGS_SIMPLE_FORMAT); } JSONObject jsonResponse = (JSONObject) obj; response.setStatus(HttpServletResponse.SC_OK); PrintWriter out = response.getWriter(); System.out.println("jsonResponse="+jsonResponse); out.println(jsonResponse.toString()); return; } else if ( "PUT".equals(request.getMethod()) ) { // This is assuming the rule that a PUT of the complex settings // format that there is only one entry in the graph and it is // the same as our current URL. We parse without much checking. String settings = null; try { JSONArray graph = (JSONArray) requestData.get(LTI2Constants.GRAPH); if ( graph.size() != 1 ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, "Only one graph entry allowed", null); return; } JSONObject firstChild = (JSONObject) graph.get(0); JSONObject custom = (JSONObject) firstChild.get(LTI2Constants.CUSTOM); settings = custom.toString(); } catch (Exception e) { settings = jsonRequest.getPostBody(); } retval = null; if ( LTI2Util.SCOPE_LtiLink.equals(scope) ) { content.put(LTIService.LTI_SETTINGS, settings); retval = ltiService.updateContentDao(contentKey,content,siteId); } else if ( LTI2Util.SCOPE_ToolProxyBinding.equals(scope) ) { if ( proxyBinding != null ) { proxyBinding.put(LTIService.LTI_SETTINGS, settings); retval = ltiService.updateProxyBindingDao(proxyBindingKey,proxyBinding); } else { Properties proxyBindingNew = new Properties(); proxyBindingNew.setProperty(LTIService.LTI_SITE_ID, siteId); proxyBindingNew.setProperty(LTIService.LTI_TOOL_ID, toolKey+""); proxyBindingNew.setProperty(LTIService.LTI_SETTINGS, settings); retval = ltiService.insertProxyBindingDao(proxyBindingNew); M_log.info("inserted ProxyBinding setting="+proxyBindingNew); } } else if ( LTI2Util.SCOPE_ToolProxy.equals(scope) ) { deploy.put(LTIService.LTI_SETTINGS, settings); retval = ltiService.updateDeployDao(deployKey,deploy); } if ( retval instanceof String || ( retval instanceof Boolean && ((Boolean) retval != Boolean.TRUE) ) ) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, (String) retval, null); return; } response.setStatus(HttpServletResponse.SC_OK); } else { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); doErrorJSON(request,response, jsonRequest, "Method not handled="+request.getMethod(), null); } } /* IMS JSON version of Errors */ public void doErrorJSON(HttpServletRequest request,HttpServletResponse response, IMSJSONRequest json, String message, Exception e) throws java.io.IOException { if (e != null) { M_log.error(e.getLocalizedMessage(), e); } M_log.info(message); String jsonText = IMSJSONRequest.doErrorJSON(request, response, json, message, e); System.out.println(jsonText); } public void destroy() { } }