Java tutorial
package org.imsglobal.lti.toolProvider; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.security.Key; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.imsglobal.lti.LTIMessage; import org.imsglobal.lti.LTIUtil; import org.imsglobal.lti.product.Product; import org.imsglobal.lti.product.ProductFamily; import org.imsglobal.lti.profile.ProfileMessage; import org.imsglobal.lti.profile.ProfileResourceHandler; import org.imsglobal.lti.profile.ServiceDefinition; import org.imsglobal.lti.toolProvider.dataConnector.DataConnector; import org.imsglobal.lti.toolProvider.mediaType.ConsumerProfile; import org.imsglobal.lti.toolProvider.mediaType.JSONContext; import org.imsglobal.lti.toolProvider.mediaType.MediaTypeToolProxy; import org.imsglobal.lti.toolProvider.mediaType.ToolService; import org.joda.time.DateTime; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import net.oauth.OAuth; import net.oauth.OAuthAccessor; import net.oauth.OAuthConsumer; import net.oauth.OAuthMessage; import net.oauth.OAuthProblemException; import net.oauth.OAuthValidator; import net.oauth.SimpleOAuthValidator; import net.oauth.server.HttpRequestMessage; import net.oauth.server.OAuthServlet; import net.oauth.signature.OAuthSignatureMethod; public class ToolProvider { /** * Class to represent an LTI Tool Provider * * @author Stephen P Vickers <svickers@imsglobal.org> * @copyright IMS Global Learning Consortium Inc * @date 2016 * @version 3.0.2 * @license GNU Lesser General Public License, version 3 (<http://www.gnu.org/licenses/lgpl.html>) */ /** * Default connection error message. */ public static final String CONNECTION_ERROR_MESSAGE = "Sorry, there was an error connecting you to the application."; /** * LTI version 1 for messages. */ public static final String LTI_VERSION1 = "LTI-1p0"; /** * LTI version 2 for messages. */ public static final String LTI_VERSION2 = "LTI-2p0"; /** * Use ID value only. */ public static final int ID_SCOPE_ID_ONLY = 0; /** * Prefix an ID with the consumer key. */ public static final int ID_SCOPE_GLOBAL = 1; /** * Prefix the ID with the consumer key and context ID. */ public static final int ID_SCOPE_CONTEXT = 2; /** * Prefix the ID with the consumer key and resource ID. */ public static final int ID_SCOPE_RESOURCE = 3; /** * Character used to separate each element of an ID. */ public static final String ID_SCOPE_SEPARATOR = ":"; /** * Permitted LTI versions for messages. */ private static final List<String> LTI_VERSIONS = Arrays.asList(LTI_VERSION1, LTI_VERSION2); /** * List of supported message types and associated class methods. */ private static final Map<String, String> MESSAGE_TYPES; static { Map<String, String> aMap = new HashMap<String, String>(); aMap.put("basic-lti-launch-request", "onLaunch"); aMap.put("ContentItemSelectionRequest", "onContentItem"); aMap.put("ToolProxyRegistrationRequest", "register"); MESSAGE_TYPES = Collections.unmodifiableMap(aMap); } /** * List of supported message types and associated class methods * * @var array METHOD_NAMES */ private static final Map<String, String> METHOD_NAMES; static { Map<String, String> bMap = new HashMap<String, String>(); bMap.put("basic-lti-launch-request", "onLaunch"); bMap.put("ContentItemSelectionRequest", "onContentItem"); bMap.put("ToolProxyRegistrationRequest", "onRegister"); METHOD_NAMES = Collections.unmodifiableMap(bMap); } /** * Names of LTI parameters to be retained in the consumer settings property. * * @var array LTI_CONSUMER_SETTING_NAMES */ private static String[] LTI_CONSUMER_SETTING_NAMES = { "custom_tc_profile_url", "custom_system_setting_url" }; /** * Names of LTI parameters to be retained in the context settings property. * * @var array LTI_CONTEXT_SETTING_NAMES */ private static String[] LTI_CONTEXT_SETTING_NAMES = { "custom_context_setting_url", "custom_lineitems_url", "custom_results_url", "custom_context_memberships_url" }; /** * Names of LTI parameters to be retained in the resource link settings property. * * @var array LTI_RESOURCE_LINK_SETTING_NAMES */ private static String[] LTI_RESOURCE_LINK_SETTING_NAMES = { "lis_result_sourcedid", "lis_outcome_service_url", "ext_ims_lis_basic_outcome_url", "ext_ims_lis_resultvalue_sourcedids", "ext_ims_lis_memberships_id", "ext_ims_lis_memberships_url", "ext_ims_lti_tool_setting", "ext_ims_lti_tool_setting_id", "ext_ims_lti_tool_setting_url", "custom_link_setting_url", "custom_lineitem_url", "custom_result_url" }; /** * Names of LTI custom parameter substitution variables (or capabilities) and their associated default message parameter names. * * @var array CUSTOM_SUBSTITUTION_VARIABLES */ private static final Map<String, String> CUSTOM_SUBSTITUTION_VARIABLES; static { Map<String, String> cMap = new HashMap<String, String>(); cMap.put("User.id", "user_id"); cMap.put("User.image", "user_image"); cMap.put("User.username", "username"); cMap.put("User.scope.mentor", "role_scope_mentor"); cMap.put("Membership.role", "roles"); cMap.put("Person.sourcedId", "lis_person_sourcedid"); cMap.put("Person.name.full", "lis_person_name_full"); cMap.put("Person.name.family", "lis_person_name_family"); cMap.put("Person.name.given", "lis_person_name_given"); cMap.put("Person.email.primary", "lis_person_contact_email_primary"); cMap.put("Context.id", "context_id"); cMap.put("Context.type", "context_type"); cMap.put("Context.title", "context_title"); cMap.put("Context.label", "context_label"); cMap.put("CourseOffering.sourcedId", "lis_course_offering_sourcedid"); cMap.put("CourseSection.sourcedId", "lis_course_section_sourcedid"); cMap.put("CourseSection.label", "context_label"); cMap.put("CourseSection.title", "context_title"); cMap.put("ResourceLink.id", "resource_link_id"); cMap.put("ResourceLink.title", "resource_link_title"); cMap.put("ResourceLink.description", "resource_link_description"); cMap.put("Result.sourcedId", "lis_result_sourcedid"); cMap.put("BasicOutcome.url", "lis_outcome_service_url"); cMap.put("ToolConsumerProfile.url", "custom_tc_profile_url"); cMap.put("ToolProxy.url", "tool_proxy_url"); cMap.put("ToolProxy.custom.url", "custom_system_setting_url"); cMap.put("ToolProxyBinding.custom.url", "custom_context_setting_url"); cMap.put("LtiLink.custom.url", "custom_link_setting_url"); cMap.put("LineItems.url", "custom_lineitems_url"); cMap.put("LineItem.url", "custom_lineitem_url"); cMap.put("Results.url", "custom_results_url"); cMap.put("Result.url", "custom_result_url"); cMap.put("ToolProxyBinding.memberships.url", "custom_context_memberships_url"); CUSTOM_SUBSTITUTION_VARIABLES = Collections.unmodifiableMap(cMap); } /** * True if the last request was successful. * * @var boolean ok */ private boolean ok = true; /** * Tool Consumer object. * * @var ToolConsumer consumer */ private ToolConsumer consumer = null; /** * Return URL provided by tool consumer. * * @var URL returnUrl */ private URL returnUrl = null; /** * User object. * * @var User user */ private User user = null; /** * Resource link object. * * @var ResourceLink resourceLink */ private ResourceLink resourceLink = null; /** * Context object. * * @var Context context */ private Context context = null; /** * Data connector object. * * @var DataConnector dataConnector */ private DataConnector dataConnector = null; /** * Default email domain. * * @var string defaultEmail */ private String defaultEmail = ""; /** * Scope to use for user IDs. * * @var int idScope */ private int idScope = ID_SCOPE_ID_ONLY; /** * Whether shared resource link arrangements are permitted. * * @var boolean allowSharing */ private boolean allowSharing = false; /** * Message for last request processed * * @var string message */ private String message = CONNECTION_ERROR_MESSAGE; /** * Error message for last request processed. * * @var string reason */ private String reason = null; /** * Details for error message relating to last request processed. * * @var array details */ private List<String> details = new ArrayList<String>(); /** * Base URL for tool provider service * * @var string baseUrl */ private URL baseUrl = null; /** * Vendor details * * @var Item vendor */ private ProductFamily vendor = null; /** * Product details * * @var Item product */ public Product product = null; /** * Services required by Tool Provider * * @var array requiredServices */ private List<ToolService> requiredServices = new ArrayList<ToolService>(); /** * Optional services used by Tool Provider * * @var array optionalServices */ private List<ToolService> optionalServices = new ArrayList<ToolService>(); /** * Resource handlers for Tool Provider * * @var array resourceHandlers */ private List<ProfileResourceHandler> resourceHandlers = new ArrayList<ProfileResourceHandler>(); /** * URL to redirect user to on successful completion of the request. * * @var string redirectUrl */ private URL redirectUrl = null; /** * URL to redirect user to on successful completion of the request. * * @var string mediaTypes */ private Set<String> mediaTypes = new HashSet<String>(); /** * target for new document to be displayed * * @var string documentTargets */ private Set<String> documentTargets = new HashSet<String>(); /** * HTML to be displayed on a successful completion of the request. * * @var string output */ private String output = null; /** * HTML to be displayed on an unsuccessful completion of the request and no return URL is available. * * @var string errorOutput */ private String errorOutput = null; /** * Whether debug messages explaining the cause of errors are to be returned to the tool consumer. * * @var boolean debugMode */ private boolean debugMode = false; /** * URL to redirect user to if the request is not successful. */ private String error = null; /** * LTI parameter constraints for auto validation checks. * * @var array constraints */ private Map<String, ParameterConstraint> constraints = new HashMap<String, ParameterConstraint>(); private String messageType; private String ltiVersion; private HttpServletRequest request; private HttpServletResponse response; /** * Class constructor * * @param DataConnector dataConnector Object containing a database connection object */ public ToolProvider(DataConnector dataConnector, HttpServletRequest request, HttpServletResponse response) { this.dataConnector = dataConnector; ok = (dataConnector != null); this.request = request; this.response = response; // Set debug mode String customDebug = request.getParameter("custom_debug"); if (StringUtils.isNotEmpty(customDebug) && StringUtils.equalsIgnoreCase(customDebug, "true")) { setDebugMode(true); } // Set return URL if available try { String tryreturnUrl = request.getParameter("launch_presentation_return_url"); if (StringUtils.isNotEmpty(tryreturnUrl)) { setReturnUrl(new URL(tryreturnUrl)); } else { tryreturnUrl = request.getParameter("content_item_return_url"); if (StringUtils.isNotEmpty(tryreturnUrl)) { setReturnUrl(new URL(tryreturnUrl)); } } } catch (MalformedURLException e) { e.printStackTrace(); } setMessageType(request.getParameter("lti_message_type")); setLTIVersion(request.getParameter("lti_version")); vendor = new ProductFamily(); product = new Product(); } /** * Process an incoming request */ public void handleRequest() { if (ok) { if (authenticate()) { ok = doCallback(); } } if (ok) { result(); } else { onError(); } } /** * Add a parameter constraint to be checked on launch * * @param string name Name of parameter to be checked * @param boolean required True if parameter is required (optional, default is true) * @param int maxLength Maximum permitted length of parameter value (optional, default is null) * @param array messageTypes Array of message types to which the constraint applies (optional, default is all) */ public void setParameterConstraint(String name) { setParameterConstraint(name, true, 0, null); } public void setParameterConstraint(String name, boolean required) { setParameterConstraint(name, required, 0, null); } public void setParameterConstraint(String name, boolean required, int maxLength) { setParameterConstraint(name, required, maxLength, null); } public void setParameterConstraint(String name, boolean required, int maxLength, String[] messageTypes) { name = StringUtils.trim(name); if (name.length() > 0) { ParameterConstraint pc = new ParameterConstraint(required, maxLength); Set<String> types = new HashSet<String>(); Collections.addAll(types, messageTypes); pc.setMessageTypes(types); constraints.put(name, pc); } } /** * Get an array of defined tool consumers * * @return array Array of ToolConsumer objects */ public List<ToolConsumer> getConsumers() { return dataConnector.getToolConsumers(); } /** * Find an offered service based on a media type and HTTP action(s) * * @param string format Media type required * @param array methods Array of HTTP actions required * * @return object The service object */ public ServiceDefinition findService(String format, List<String> methods) { ServiceDefinition found = null; List<ServiceDefinition> services = consumer.getProfile().getServicesOffered(); for (ServiceDefinition service : services) { List<String> formats = service.getFormats(); if (!formats.contains(format)) { continue; } List<String> missing = new ArrayList<String>(); List<String> actions = service.getActions(); for (String method : methods) { if (!actions.contains(method)) { missing.add(method); } } if (missing.isEmpty()) { found = service; break; } } return found; } /** * Send the tool proxy to the Tool Consumer * * @return boolean True if the tool proxy was accepted */ public boolean doToolProxyService() { // Create tool proxy List<String> methods = new ArrayList<String>(); methods.add("POST"); ServiceDefinition toolProxyService = findService("application/vnd.ims.lti.v2.toolproxy+json", methods); String secret = DataConnector.getRandomString(12); MediaTypeToolProxy toolProxy = new MediaTypeToolProxy(this, toolProxyService, secret); Map<String, List<String>> proxyMap = toolProxy.toMap(); LTIMessage http = consumer.doServiceRequest(toolProxyService, "POST", "application/vnd.ims.lti.v2.toolproxy+json", proxyMap); ok = http.isOk() && (http.getStatus() == 201) && http.getResponseJson().containsKey("tool_proxy_guid") && (((String) http.getResponseJson().get("tool_proxy_guid")).length() > 0); if (ok) { consumer.setKey((String) http.getResponseJson().get("tool_proxy_guid")); consumer.setSecret(toolProxy.getSecurityContract().getSharedSecret()); consumer.setToolProxyMap(proxyMap); consumer.save(); } return ok; } /** * Get an array of fully qualified user roles * * @param mixed roles Comma-separated list of roles or array of roles * * @return array Array of roles */ public static List<String> parseRoles(String roles) { List<String> roleList = new ArrayList<String>(); for (String r : StringUtils.split(roles, ",")) { roleList.add(r); } return parseRoles(roleList); } public static List<String> parseRoles(List<String> roles) { List<String> parsedRoles = new ArrayList<String>(); for (String role : roles) { role = StringUtils.trim(role); if (StringUtils.isNotEmpty(role)) { if (role.substring(0, 4) != "urn:") { role = "urn:lti:role:ims/lis/" + role; } parsedRoles.add(role); } } return parsedRoles; } /** * Generate a web page containing an auto-submitted form of parameters. * * @param string url URL to which the form should be submitted * @param array params Array of form parameters * @param string target Name of target (optional) * @return string */ public static String sendForm(URL errorUrl, Map<String, List<String>> formParams) { return sendForm(errorUrl, formParams, ""); } public static String sendForm(URL url, Map<String, List<String>> params, String target) { String page = "<html>\n" + "<head>\n" + "<title>IMS LTI message</title>\n" + "<script type=\"text/javascript\">\n" + "//<![CDATA[\n" + "function doOnLoad() {\n" + " document.forms[0].submit();\n" + "}\n\n" + "window.onload=doOnLoad;\n" + "//]]>\n" + "</script>\n" + "</head>\n" + "<body>\n" + "<form action=\"{url}\" method=\"post\" target=\"\" encType=\"application/x-www-form-urlencoded\">\n"; for (String key : params.keySet()) { for (String value : params.get(key)) { String key2 = StringEscapeUtils.escapeHtml4(key); String value2 = StringEscapeUtils.escapeHtml4(value); page += "<input type=\"hidden\" name=\"" + key2 + "\" value=\"" + value2 + "\" />\n\n"; } } page += "</form>\n" + "</body>\n" + "</html>\n"; return page; } /// /// PROTECTED METHODS /// /** * Process a valid launch request * * @return boolean True if no error */ protected boolean onLaunch() { return onError(); } /** * Process a valid content-item request * * @return boolean True if no error */ protected boolean onContentItem() { return onError(); } /** * Process a valid tool proxy registration request * * @return boolean True if no error */ protected boolean onRegister() { return onError(); } /** * Process a response to an invalid request * * @return boolean True if no further error processing required */ protected boolean onError() { try { throw new Exception("At onError"); } catch (Exception e) { e.printStackTrace(); } System.err.println(reason); if (debugMode) { for (String detail : details) { System.err.println(detail); } } return false; } /// /// PRIVATE METHODS /// /** * Call any callback function for the requested action. * * This function may set the redirect_url and output properties. * * @return boolean True if no error reported */ private boolean doCallback() { String type = request.getParameter("lti_message_type"); if (type != null) { if (METHOD_NAMES.containsKey(type)) { return doCallback(METHOD_NAMES.get(type)); } } return false; } private boolean doCallback(String method) { String callback = method; Boolean retVal = null; if (callback == null) { callback = getMessageType(); } Class<? extends ToolProvider> clazz = this.getClass(); boolean methodExists = false; for (Method m : clazz.getDeclaredMethods()) { if (m.getName().equals(callback)) { methodExists = true; try { retVal = (Boolean) m.invoke(this); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } //returns a boolean } } if (!methodExists) { //didn"t find the method in declared methods if (StringUtils.isNotEmpty(method)) { reason = "Message type not supported: " + getMessageType(); retVal = false; } } if (retVal && (getMessageType().equals("ToolProxyRegistrationRequest"))) { consumer.save(); } return retVal; } /** * Perform the result of an action. * * This function may redirect the user to another URL rather than returning a value. * * @return string Output to be displayed (redirection, or display HTML or message) */ private void result() { if (!ok) { ok = onError(); } if (!ok) { try { if (returnUrl != null) { URIBuilder errorUrlBuilder = new URIBuilder(returnUrl.toExternalForm()); //add necessary query parameters to error url //first, figure out if we know the reason HashMap<String, String> params = new HashMap<String, String>(); if (debugMode && StringUtils.isNotEmpty(reason)) { params.put("lti_errormsg", "Debug error: " + reason); } else { params.put("lti_errormsg", message); if (StringUtils.isNotEmpty(reason)) { params.put("lti_errorlog", "Debug error: " + reason); } } for (String key : params.keySet()) { errorUrlBuilder.addParameter(key, params.get(key)); } URL errorUrl; errorUrl = errorUrlBuilder.build().toURL(); if (consumer != null && request != null) { String ltiMessageType = request.getParameter("lti_message_type"); if (ltiMessageType.equals("ContentItemSelectionRequest")) { String version = request.getParameter("lti_version"); if (version == null) { version = LTI_VERSION1; } Map<String, List<String>> toSend = new HashMap<String, List<String>>(); Map<String, String[]> formParams = request.getParameterMap(); for (String key : formParams.keySet()) { for (String v : formParams.get(key)) { LTIUtil.setParameter(toSend, key, v); } } Map<String, List<String>> signedParams = consumer.signParameters( errorUrl.toExternalForm(), "ContentItemSelection", version, "POST", toSend); String page = sendForm(errorUrl, signedParams); System.out.print(page); } else { System.err.println("Attempt to redirect to " + errorUrl); response.sendRedirect(errorUrl.toExternalForm()); } } else { try { System.err.println("Attempt to redirect to " + errorUrl); response.sendRedirect(errorUrl.toExternalForm()); } catch (IOException ioe) { return; } } return; } else { if (errorOutput != null) { System.err.println(errorOutput); } else if (debugMode && StringUtils.isNotEmpty(reason)) { System.err.println("Debug error: " + reason); } else { System.err.println("Error: " + message); } } } catch (MalformedURLException e) { e.printStackTrace(); } catch (URISyntaxException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } else if (this.redirectUrl != null) { try { System.err.println("Attempt to redirect to " + redirectUrl); this.response.sendRedirect(this.redirectUrl.toExternalForm()); } catch (IOException ioe) { return; } return; } else if (output != null) { System.out.println(output); } else { System.out.println("Successful handling of request finished."); } } /** * Check the authenticity of the LTI launch request. * * The consumer, resource link and user objects will be initialised if the request is valid. * * @return boolean True if the request has been successfully validated. */ private boolean authenticate() { boolean doSaveConsumer = false; //JSON Initialization JSONParser parser = new JSONParser(); JSONObject tcProfile = null; String messageType = request.getParameter("lti_message_type"); ok = (StringUtils.isNotEmpty(messageType) && MESSAGE_TYPES.containsKey(messageType)); if (!ok) { reason = "Invalid or missing lti_message_type parameter."; } if (ok) { String version = request.getParameter("lti_version"); ok = (StringUtils.isNotEmpty(version)) && (LTI_VERSIONS.contains(version)); if (!ok) { reason = "Invalid or missing lti_version parameter."; } } if (ok) { if (messageType.equals("basic-lti-launch-request")) { String resLinkId = request.getParameter("resource_link_id"); ok = (StringUtils.isNotEmpty(resLinkId)); if (!ok) { reason = "Missing resource link ID."; } } else if (messageType.equals("ContentItemSelectionRequest")) { String accept = request.getParameter("accept_media_types"); ok = (StringUtils.isNotEmpty(accept)); if (ok) { String[] mediaTypes = StringUtils.split(accept, ","); Set<String> mTypes = new HashSet<String>(); for (String m : mediaTypes) { mTypes.add(m); //unique set of media types, no repeats } ok = mTypes.size() > 0; if (!ok) { reason = "No accept_media_types found."; } else { this.mediaTypes = mTypes; } } else { //no accept ok = false; } String targets = request.getParameter("accept_presentation_document_targets"); if (ok && StringUtils.isNotEmpty(targets)) { Set<String> documentTargets = new HashSet<String>(); for (String t : StringUtils.split(targets, ",")) { documentTargets.add(t); } ok = documentTargets.size() > 0; if (!ok) { reason = "Missing or empty accept_presentation_document_targets parameter."; } else { List<String> valid = Arrays.asList("embed", "frame", "iframe", "window", "popup", "overlay", "none"); boolean thisCheck = true; String problem = ""; for (String target : documentTargets) { thisCheck = valid.contains(target); if (!thisCheck) { problem = target; break; } } if (!thisCheck) { reason = "Invalid value in accept_presentation_document_targets parameter: " + problem; } } if (ok) { this.documentTargets = documentTargets; } } else { ok = false; } if (ok) { String ciReturnUrl = request.getParameter("content_item_return_url"); ok = StringUtils.isNotEmpty(ciReturnUrl); if (!ok) { reason = "Missing content_item_return_url parameter."; } } } else if (messageType.equals("ToolProxyRegistrationRequest")) { String regKey = request.getParameter("reg_key"); String regPass = request.getParameter("reg_password"); String profileUrl = request.getParameter("tc_profile_url"); String launchReturnUrl = request.getParameter("launch_presentation_return_url"); ok = StringUtils.isNotEmpty(regKey) && StringUtils.isNotEmpty(regPass) && StringUtils.isNotEmpty(profileUrl) && StringUtils.isNotEmpty(launchReturnUrl); if (debugMode && !ok) { reason = "Missing message parameters."; } } } DateTime now = DateTime.now(); //check consumer key if (ok && !messageType.equals("ToolProxyRegistrationRequest")) { String key = request.getParameter("oauth_consumer_key"); ok = StringUtils.isNotEmpty(key); if (!ok) { reason = "Missing consumer key."; } if (ok) { this.consumer = new ToolConsumer(key, dataConnector); ok = consumer.getCreated() != null; if (!ok) { reason = "Invalid consumer key."; } } if (ok) { if (consumer.getLastAccess() == null) { doSaveConsumer = true; } else { DateTime last = consumer.getLastAccess(); doSaveConsumer = doSaveConsumer || last.isBefore(now.withTimeAtStartOfDay()); } consumer.setLastAccess(now); String baseString = ""; String signature = ""; OAuthMessage oAuthMessage = null; try { OAuthConsumer oAuthConsumer = new OAuthConsumer("about:blank", consumer.getKey(), consumer.getSecret(), null); OAuthAccessor oAuthAccessor = new OAuthAccessor(oAuthConsumer); OAuthValidator oAuthValidator = new SimpleOAuthValidator(); URL u = new URL(request.getRequestURI()); String url = u.getProtocol() + "://" + u.getHost() + u.getPath(); Map<String, String[]> params = request.getParameterMap(); Map<String, List<String>> param2 = new HashMap<String, List<String>>(); for (String k : params.keySet()) { param2.put(k, Arrays.asList(params.get(k))); } List<Map.Entry<String, String>> param3 = ToolConsumer.convert(param2); oAuthMessage = new OAuthMessage(request.getMethod(), url, param3); baseString = OAuthSignatureMethod.getBaseString(oAuthMessage); signature = oAuthMessage.getSignature(); oAuthValidator.validateMessage(oAuthMessage, oAuthAccessor); } catch (Exception e) { System.err.println(e.getMessage()); OAuthProblemException oe = null; if (e instanceof OAuthProblemException) { oe = (OAuthProblemException) e; for (String p : oe.getParameters().keySet()) { System.err.println(p + ": " + oe.getParameters().get(p).toString()); } } this.ok = false; if (StringUtils.isEmpty(reason)) { if (debugMode) { reason = e.getMessage(); if (StringUtils.isEmpty(reason)) { reason = "OAuth exception."; } details.add("Timestamp: " + request.getParameter("oauth_timestamp")); details.add("Current system time: " + System.currentTimeMillis()); details.add("Signature: " + signature); details.add("Base string: " + baseString); } else { reason = "OAuth signature check failed - perhaps an incorrect secret or timestamp."; } } } } if (ok) { DateTime today = DateTime.now(); if (consumer.getLastAccess() == null) { doSaveConsumer = true; } else { DateTime last = consumer.getLastAccess(); doSaveConsumer = doSaveConsumer || last.isBefore(today.withTimeAtStartOfDay()); } consumer.setLastAccess(today); if (consumer.isThisprotected()) { String guid = request.getParameter("tool_consumer_instance_guid"); if (StringUtils.isNotEmpty(consumer.getConsumerGuid())) { ok = StringUtils.isEmpty(guid) || consumer.getConsumerGuid().equals(guid); if (!ok) { reason = "Request is from an invalid tool consumer."; } } else { ok = StringUtils.isEmpty(guid); if (!ok) { reason = "A tool consumer GUID must be included in the launch request."; } } } if (ok) { ok = consumer.isEnabled(); if (!ok) { reason = "Tool consumer has not been enabled by the tool provider."; } } if (ok) { ok = consumer.getEnableFrom() == null || consumer.getEnableFrom().isBefore(today); if (ok) { ok = consumer.getEnableUntil() == null || consumer.getEnableUntil().isAfter(today); if (!ok) { reason = "Tool consumer access has expired."; } } else { reason = "Tool consumer access is not yet available."; } } } // Validate other message parameter values if (ok) { List<String> boolValues = Arrays.asList("true", "false"); String acceptUnsigned = request.getParameter("accept_unsigned"); String acceptMultiple = request.getParameter("accept_multiple"); String acceptCopyAdvice = request.getParameter("accept_copy_advice"); String autoCreate = request.getParameter("auto_create"); String canConfirm = request.getParameter("can_confirm"); String lpdt = request.getParameter("launch_presentation_document_target"); if (messageType.equals("ContentItemSelectionRequest")) { if (StringUtils.isNotEmpty(acceptUnsigned)) { ok = boolValues.contains(acceptUnsigned); if (!ok) { reason = "Invalid value for accept_unsigned parameter: " + acceptUnsigned + "."; } } if (ok && StringUtils.isNotEmpty(acceptMultiple)) { ok = boolValues.contains(acceptMultiple); if (!ok) { reason = "Invalid value for accept_multiple parameter: " + acceptMultiple + "."; } } if (ok && StringUtils.isNotEmpty(acceptCopyAdvice)) { ok = boolValues.contains(acceptCopyAdvice); if (!ok) { reason = "Invalid value for accept_copy_advice parameter: " + acceptCopyAdvice + "."; } } if (ok && StringUtils.isNotEmpty(autoCreate)) { ok = boolValues.contains(autoCreate); if (!ok) { reason = "Invalid value for auto_create parameter: " + autoCreate + "."; } } if (ok && StringUtils.isNotEmpty(canConfirm)) { ok = boolValues.contains(canConfirm); if (!ok) { reason = "Invalid value for can_confirm parameter: " + canConfirm + "."; } } } else if (StringUtils.isNotEmpty(lpdt)) { List<String> valid = Arrays.asList("embed", "frame", "iframe", "window", "popup", "overlay", "none"); ok = valid.contains(lpdt); if (!ok) { reason = "Invalid value for launch_presentation_document_target parameter: " + lpdt + "."; } } } } if (ok && (messageType.equals("ToolProxyRegistrationRequest"))) { ok = request.getParameter("lti_version").equals(LTI_VERSION2); if (!ok) { reason = "Invalid lti_version parameter"; } if (ok) { HttpClient client = HttpClientBuilder.create().build(); String tcProfUrl = request.getParameter("tc_profile_url"); HttpGet get = new HttpGet(tcProfUrl); get.addHeader("Accept", "application/vnd.ims.lti.v2.toolconsumerprofile+json"); HttpResponse response = null; try { response = client.execute(get); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } if (response == null) { reason = "Tool consumer profile not accessible."; } else { try { String json_string = EntityUtils.toString(response.getEntity()); tcProfile = (JSONObject) parser.parse(json_string); ok = tcProfile != null; if (!ok) { reason = "Invalid JSON in tool consumer profile."; } else { JSONContext jsonContext = new JSONContext(); jsonContext.parse(tcProfile); consumer.getProfile().setContext(jsonContext); } } catch (Exception je) { je.printStackTrace(); ok = false; reason = "Invalid JSON in tool consumer profile."; } } } // Check for required capabilities if (ok) { String regKey = request.getParameter("reg_key"); consumer = new ToolConsumer(regKey, dataConnector); ConsumerProfile profile = new ConsumerProfile(); JSONContext context = new JSONContext(); context.parse(tcProfile); profile.setContext(context); consumer.setProfile(profile); List<String> capabilities = consumer.getProfile().getCapabilityOffered(); List<String> missing = new ArrayList<String>(); for (ProfileResourceHandler handler : resourceHandlers) { for (ProfileMessage message : handler.getRequiredMessages()) { String type = message.getType(); if (!capabilities.contains(type)) { missing.add(type); } } } for (String name : constraints.keySet()) { ParameterConstraint constraint = constraints.get(name); if (constraint.isRequired()) { if (!capabilities.contains(name)) { missing.add(name); } } } if (!missing.isEmpty()) { StringBuilder sb = new StringBuilder(); for (String cap : missing) { sb.append(cap).append(", "); } reason = "Required capability not offered - \"" + sb.toString() + "\""; ok = false; } } // Check for required services if (ok) { for (ToolService tService : requiredServices) { for (String format : tService.getFormats()) { ServiceDefinition sd = findService(format, tService.getActions()); if (sd == null) { if (ok) { reason = "Required service(s) not offered - "; ok = false; } else { reason += ", "; } StringBuilder sb = new StringBuilder(); for (String a : tService.getActions()) { sb.append(a).append(", "); } reason += format + "[" + sb.toString() + "]"; } } } } if (ok) { if (messageType.equals("ToolProxyRegistrationRequest")) { ConsumerProfile profile = new ConsumerProfile(); JSONContext context = new JSONContext(); context.parse(tcProfile); profile.setContext(context); consumer.setProfile(profile); consumer.setSecret(request.getParameter("reg_password")); consumer.setLtiVersion(request.getParameter("lti_version")); consumer.setName(profile.getProduct().getProductInfo().getProductName().get("default_name")); consumer.setConsumerName(consumer.getName()); consumer.setConsumerVersion( "{tcProfile.product_instance.product_info.product_family.code}-{tcProfile.product_instance.product_info.product_version}"); consumer.setConsumerGuid(profile.getProduct().getGuid()); consumer.setEnabled(true); consumer.setThisprotected(true); doSaveConsumer = true; } } } else if (ok && !request.getParameter("custom_tc_profile_url").isEmpty() && consumer.getProfile() == null) { String tcProfUrl = request.getParameter("custom_tc_profile_url"); HttpClient client = HttpClientBuilder.create().build(); HttpGet get = new HttpGet(tcProfUrl); get.addHeader("Accept", "application/vnd.ims.lti.v2.toolconsumerprofile+json"); HttpResponse response = null; try { response = client.execute(get); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } if (response == null) { reason = "Tool consumer profile not accessible."; } else { try { String json_string = EntityUtils.toString(response.getEntity()); tcProfile = (JSONObject) parser.parse(json_string); ok = tcProfile != null; if (!ok) { reason = "Invalid JSON in tool consumer profile."; } else { JSONContext jsonContext = new JSONContext(); jsonContext.parse(tcProfile); consumer.getProfile().setContext(jsonContext); doSaveConsumer = true; } } catch (Exception je) { je.printStackTrace(); ok = false; reason = "Invalid JSON in tool consumer profile."; } } } // Validate message parameter constraints if (ok) { List<String> invalidParameters = new ArrayList<String>(); for (String name : constraints.keySet()) { ParameterConstraint constraint = constraints.get(name); if (constraint.getMessageTypes().isEmpty() || constraint.getMessageTypes().contains(messageType)) { ok = true; String n = request.getParameter("name"); if (constraint.isRequired()) { n = n.trim(); if (StringUtils.isBlank(n)) { invalidParameters.add(name + " (missing)"); ok = false; } } if (ok && constraint.getMaxLength() > 0 && StringUtils.isNotBlank(n)) { if (n.trim().length() > constraint.getMaxLength()) { invalidParameters.add(name + " (too long)"); } } } } if (invalidParameters.size() > 0) { ok = false; if (StringUtils.isEmpty(reason)) { StringBuilder sb = new StringBuilder(); for (String ip : invalidParameters) { sb.append(ip).append(", "); } reason = "Invalid parameter(s): " + sb.toString() + "."; } } } if (ok) { // Set the request context String cId = request.getParameter("context_id"); if (StringUtils.isNotEmpty(cId)) { context = Context.fromConsumer(consumer, cId.trim()); String title = request.getParameter("context_title"); if (StringUtils.isNotEmpty(title)) { title = title.trim(); } if (StringUtils.isBlank(title)) { title = "Course " + context.getId(); } context.setTitle(title); } // Set the request resource link String rlId = request.getParameter("resource_link_id"); if (StringUtils.isNotEmpty(rlId)) { String contentItemId = request.getParameter("custom_content_item_id"); resourceLink = ResourceLink.fromConsumer(consumer, rlId, contentItemId); if (context != null) { resourceLink.setContextId(context.getRecordId()); } String title = request.getParameter("resource_link_title"); if (StringUtils.isEmpty(title)) { title = "Resource " + resourceLink.getId(); } resourceLink.setTitle(title); // Delete any existing custom parameters for (String name : consumer.getSettings().keySet()) { if (StringUtils.startsWith(name, "custom_")) { consumer.setSetting(name); doSaveConsumer = true; } } if (context != null) { for (String name : context.getSettings().keySet()) { if (StringUtils.startsWith(name, "custom_")) { context.setSetting(name, ""); } } } for (String name : resourceLink.getSettings().keySet()) { if (StringUtils.startsWith(name, "custom_")) { resourceLink.setSetting(name, ""); } } // Save LTI parameters for (String name : LTI_CONSUMER_SETTING_NAMES) { String s = request.getParameter(name); if (StringUtils.isNotBlank(s)) { consumer.setSetting(name, s); } else { consumer.setSetting(name); } } if (context != null) { for (String name : LTI_CONTEXT_SETTING_NAMES) { String s = request.getParameter(name); if (StringUtils.isNotBlank(s)) { context.setSetting(name, s); } else { context.setSetting(name, ""); } } } for (String name : LTI_RESOURCE_LINK_SETTING_NAMES) { String sn = request.getParameter(name); if (StringUtils.isNotEmpty(sn)) { resourceLink.setSetting(name, sn); } else { resourceLink.setSetting(name, ""); } } // Save other custom parameters ArrayList<String> combined = new ArrayList<String>(); combined.addAll(Arrays.asList(LTI_CONSUMER_SETTING_NAMES)); combined.addAll(Arrays.asList(LTI_CONTEXT_SETTING_NAMES)); combined.addAll(Arrays.asList(LTI_RESOURCE_LINK_SETTING_NAMES)); for (String name : request.getParameterMap().keySet()) { String value = request.getParameter(name); if (StringUtils.startsWith(name, "custom_") && !combined.contains(name)) { resourceLink.setSetting(name, value); } } } // Set the user instance String userId = request.getParameter("user_id"); if (StringUtils.isNotEmpty(userId)) { userId = userId.trim(); } user = User.fromResourceLink(resourceLink, userId); // Set the user name String firstname = request.getParameter("lis_person_name_given"); String lastname = request.getParameter("lis_person_name_family"); String fullname = request.getParameter("lis_person_name_full"); user.setNames(firstname, lastname, fullname); // Set the user email String email = request.getParameter("lis_person_contact_email_primary"); user.setEmail(email, defaultEmail); // Set the user image URI String img = request.getParameter("user_image"); if (StringUtils.isNotEmpty(img)) { try { URL imgUrl = new URL(img); user.setImage(imgUrl); } catch (Exception e) { //bad url } } // Set the user roles String roles = request.getParameter("roles"); if (StringUtils.isNotEmpty(roles)) { user.setRoles(parseRoles(roles)); } // Initialise the consumer and check for changes consumer.setDefaultEmail(defaultEmail); String ltiV = request.getParameter("lti_version"); if (!ltiV.equals(consumer.getLtiVersion())) { consumer.setLtiVersion(ltiV); doSaveConsumer = true; } String instanceName = request.getParameter("tool_consumer_instance_name"); if (StringUtils.isNotEmpty(instanceName)) { if (!instanceName.equals(consumer.getConsumerName())) { consumer.setConsumerName(instanceName); doSaveConsumer = true; } } String familyCode = request.getParameter("tool_consumer_info_product_family_code"); String extLMS = request.getParameter("ext_lms"); if (StringUtils.isNotBlank(familyCode)) { String version = familyCode; String infoVersion = request.getParameter("tool_consumer_info_version"); if (StringUtils.isNotEmpty(infoVersion)) { version += "-" + infoVersion; } // do not delete any existing consumer version if none is passed if (!version.equals(consumer.getConsumerVersion())) { consumer.setConsumerVersion(version); doSaveConsumer = true; } } else if (StringUtils.isNotEmpty(extLMS) && !consumer.getConsumerName().equals(extLMS)) { consumer.setConsumerVersion(extLMS); doSaveConsumer = true; } String tciGuid = request.getParameter("tool_consumer_instance_guid"); if (StringUtils.isNotEmpty(tciGuid)) { if (StringUtils.isNotEmpty(consumer.getConsumerGuid())) { consumer.setConsumerGuid(tciGuid); doSaveConsumer = true; } else if (!consumer.isThisprotected()) { doSaveConsumer = (!tciGuid.equals(consumer.getConsumerGuid())); if (doSaveConsumer) { consumer.setConsumerGuid(tciGuid); } } } String css = request.getParameter("launch_presentation_css_url"); String extCss = request.getParameter("ext_launch_presentation_css_url"); if (StringUtils.isNotEmpty(css)) { if (!css.equals(consumer.getCssPath())) { consumer.setCssPath(css); doSaveConsumer = true; } } else if (StringUtils.isNotEmpty(extCss) && !consumer.getCssPath().equals(extCss)) { consumer.setCssPath(extCss); doSaveConsumer = true; } else if (StringUtils.isNotEmpty(consumer.getCssPath())) { consumer.setCssPath(null); doSaveConsumer = true; } } // Persist changes to consumer if (doSaveConsumer) { consumer.save(); } if (ok && context != null) { context.save(); } if (ok && resourceLink != null) { // Check if a share arrangement is in place for this resource link ok = checkForShare(); // Persist changes to resource link resourceLink.save(); // Save the user instance String lrsdid = request.getParameter("lis_result_sourcedid"); if (StringUtils.isNotEmpty(lrsdid)) { if (!lrsdid.equals(user.getLtiResultSourcedId())) { user.setLtiResultSourcedId(lrsdid); user.save(); } } else if (StringUtils.isNotEmpty(user.getLtiResultSourcedId())) { user.setLtiResultSourcedId(""); user.save(); } } return ok; } /** * Check if a share arrangement is in place. * * @return boolean True if no error is reported */ private boolean checkForShare() { ok = true; boolean doSaveResourceLink = true; String key = this.resourceLink.getPrimaryConsumerKey(); int id = this.resourceLink.getPrimaryResourceLinkId(); String shareKeyValue = this.request.getParameter("custom_share_key"); boolean isShareRequest = (shareKeyValue != null) && (shareKeyValue.length() > 0); if (isShareRequest) { if (!allowSharing) { ok = false; reason = "Your sharing request has been refused because sharing is not being permitted."; } else { // Check if this is a new share key ResourceLinkShareKey shareKey = new ResourceLinkShareKey(resourceLink, shareKeyValue); if ((shareKey.getPrimaryConsumerKey() != null) && (shareKey.getResourceLinkId() != 0)) { // Update resource link with sharing primary resource link details key = shareKey.getPrimaryConsumerKey(); id = shareKey.getResourceLinkId(); ok = (!key.equals(consumer.getKey()) || !(id == resourceLink.getRecordId())); if (ok) { resourceLink.setPrimaryConsumerKey(key); resourceLink.setPrimaryResourceLinkId(id); resourceLink.setShareApproved(shareKey.isAutoApprove()); ok = resourceLink.save(); if (ok) { doSaveResourceLink = false; user.getResourceLink().setPrimaryConsumerKey(key); user.getResourceLink().setPrimaryResourceLinkId(id); user.getResourceLink().setShareApproved(shareKey.isAutoApprove()); user.getResourceLink().setUpdated(DateTime.now()); // Remove share key shareKey.delete(); } else { reason = "An error occurred initialising your share arrangement."; } } else { reason = "It is not possible to share your resource link with yourself."; } } if (ok) { ok = StringUtils.isNotEmpty(key); if (!ok) { reason = "You have requested to share a resource link but none is available."; } else { ok = (user.getResourceLink().isShareApproved()); if (!ok) { reason = "Your share request is waiting to be approved."; } } } } } else { // Check no share is in place ok = id == 0; if (!ok) { reason = "You have not requested to share a resource link but an arrangement is currently in place."; } } // Look up primary resource link if (ok && id != 0) { consumer = new ToolConsumer(key, dataConnector); ok = (consumer.getCreated() != null); if (ok) { resourceLink = ResourceLink.fromConsumerWithPK(consumer, id); ok = (resourceLink.getCreated() != null); } if (ok) { if (doSaveResourceLink) { resourceLink.save(); } } else { reason = "Unable to load resource link being shared."; } } return ok; } public ToolConsumer getConsumer() { return this.consumer; } public Product getProduct() { return product; } public void setProduct(Product product) { this.product = product; } public List<ProfileResourceHandler> getResourceHandlers() { return resourceHandlers; } public void setResourceHandlers(List<ProfileResourceHandler> resourceHandlers) { this.resourceHandlers = resourceHandlers; } public ProductFamily getVendor() { return vendor; } public void setVendor(ProductFamily vendor) { this.vendor = vendor; } public URL getBaseUrl() { return baseUrl; } public void setBaseUrl(URL baseUrl) { this.baseUrl = baseUrl; } public List<ToolService> getRequiredServices() { return requiredServices; } public void setRequiredServices(List<ToolService> requiredServices) { this.requiredServices = requiredServices; } public List<ToolService> getOptionalServices() { return optionalServices; } public void setOptionalServices(List<ToolService> optionalServices) { this.optionalServices = optionalServices; } public boolean isOk() { return ok; } public void setOk(boolean ok) { this.ok = ok; } public URL getReturnUrl() { return returnUrl; } public void setReturnUrl(URL returnUrl) { this.returnUrl = returnUrl; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public ResourceLink getResourceLink() { return resourceLink; } public void setResourceLink(ResourceLink resourceLink) { this.resourceLink = resourceLink; } public DataConnector getDataConnector() { return dataConnector; } public void setDataConnector(DataConnector dataConnector) { this.dataConnector = dataConnector; } public String getDefaultEmail() { return defaultEmail; } public void setDefaultEmail(String defaultEmail) { this.defaultEmail = defaultEmail; } public int getIdScope() { return idScope; } public void setIdScope(int idScope) { this.idScope = idScope; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public List<String> getDetails() { return details; } public void setDetails(List<String> details) { this.details = details; } public URL getRedirectUrl() { return redirectUrl; } public void setRedirectUrl(URL redirectUrl) { this.redirectUrl = redirectUrl; } public Set<String> getMediaTypes() { return mediaTypes; } public void setMediaTypes(Set<String> mediaTypes) { this.mediaTypes = mediaTypes; } public Set<String> getDocumentTargets() { return documentTargets; } public void setDocumentTargets(Set<String> documentTargets) { this.documentTargets = documentTargets; } public String getOutput() { return output; } public void setOutput(String output) { this.output = output; } public boolean isDebugMode() { return debugMode; } public void setDebugMode(boolean debugMode) { this.debugMode = debugMode; } public String getMessageType() { return messageType; } public void setMessageType(String messageType) { this.messageType = messageType; } public String getLTIVersion() { return ltiVersion; } public void setLTIVersion(String version) { this.ltiVersion = version; } }