/** * pretty much a helper function to set up the request */ public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters = NULL) { $parameters = $parameters ? $parameters : array(); $defaults = array("oauth_version" => OAuthRequest::$version, "oauth_nonce" => OAuthRequest::generate_nonce(), "oauth_timestamp" => OAuthRequest::generate_timestamp(), "oauth_consumer_key" => $consumer->key); if ($token) { $defaults['oauth_token'] = $token->key; } $parameters = array_merge($defaults, $parameters); return new OAuthRequest($http_method, $http_url, $parameters); }
/** * Add the OAuth signature to an LTI message. * * @param string $url URL for message request * @param string $type LTI message type * @param string $version LTI version * @param array $params Message parameters * * @return array Array of signed message parameters */ public function signParameters($url, $type, $version, $params) { if (!empty($url)) { // Check for query parameters which need to be included in the signature $query_params = array(); $query_string = parse_url($url, PHP_URL_QUERY); if (!is_null($query_string)) { $query_items = explode('&', $query_string); foreach ($query_items as $item) { if (strpos($item, '=') !== FALSE) { list($name, $value) = explode('=', $item); $query_params[urldecode($name)] = urldecode($value); } else { $query_params[urldecode($item)] = ''; } } } $params = $params + $query_params; // Add standard parameters $params['lti_version'] = $version; $params['lti_message_type'] = $type; $params['oauth_callback'] = 'about:blank'; // Add OAuth signature $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); $consumer = new OAuthConsumer($this->getKey(), $this->secret, NULL); $req = OAuthRequest::from_consumer_and_token($consumer, NULL, 'POST', $url, $params); $req->sign_request($hmac_method, $consumer, NULL); $params = $req->get_parameters(); // Remove parameters being passed on the query string foreach (array_keys($query_params) as $name) { unset($params[$name]); } } return $params; }
/** * 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 function authenticate() { # ### Get the consumer # $doSaveConsumer = FALSE; // Check all required launch parameters $this->isOK = isset($_POST['lti_message_type']) && array_key_exists($_POST['lti_message_type'], $this->messageTypes); if (!$this->isOK) { $this->reason = 'Invalid or missing lti_message_type parameter.'; } if ($this->isOK) { $this->isOK = isset($_POST['lti_version']) && in_array($_POST['lti_version'], $this->LTI_VERSIONS); if (!$this->isOK) { $this->reason = 'Invalid or missing lti_version parameter.'; } } if ($this->isOK) { if ($_POST['lti_message_type'] == 'basic-lti-launch-request' || $_POST['lti_message_type'] == 'DashboardRequest') { $this->isOK = isset($_POST['resource_link_id']) && strlen(trim($_POST['resource_link_id'])) > 0; if (!$this->isOK) { $this->reason = 'Missing resource link ID.'; } } else { if ($_POST['lti_message_type'] == 'ContentItemSelectionRequest') { if (isset($_POST['accept_media_types']) && strlen(trim($_POST['accept_media_types'])) > 0) { $mediaTypes = array_filter(explode(',', str_replace(' ', '', $_POST['accept_media_types'])), 'strlen'); $mediaTypes = array_unique($mediaTypes); $this->isOK = count($mediaTypes) > 0; if (!$this->isOK) { $this->reason = 'No accept_media_types found.'; } else { $this->mediaTypes = $mediaTypes; } } else { $this->isOK = FALSE; } if ($this->isOK && isset($_POST['accept_presentation_document_targets']) && strlen(trim($_POST['accept_presentation_document_targets'])) > 0) { $documentTargets = array_filter(explode(',', str_replace(' ', '', $_POST['accept_presentation_document_targets'])), 'strlen'); $documentTargets = array_unique($documentTargets); $this->isOK = count($documentTargets) > 0; if (!$this->isOK) { $this->reason = 'Missing or empty accept_presentation_document_targets parameter.'; } else { foreach ($documentTargets as $documentTarget) { $this->isOK = $this->checkValue($documentTarget, array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'), 'Invalid value in accept_presentation_document_targets parameter: %s.'); if (!$this->isOK) { break; } } if ($this->isOK) { $this->documentTargets = $documentTargets; } } } else { $this->isOK = FALSE; } if ($this->isOK) { $this->isOK = isset($_POST['content_item_return_url']) && strlen(trim($_POST['content_item_return_url'])) > 0; if (!$this->isOK) { $this->reason = 'Missing content_item_return_url parameter.'; } } } } } // Check consumer key if ($this->isOK) { $this->isOK = isset($_POST['oauth_consumer_key']); if (!$this->isOK) { $this->reason = 'Missing consumer key.'; } } if ($this->isOK) { $this->consumer = new LTI_Tool_Consumer($_POST['oauth_consumer_key'], $this->data_connector); $this->isOK = !is_null($this->consumer->created); if (!$this->isOK) { $this->reason = 'Invalid consumer key.'; } } $now = time(); if ($this->isOK) { $today = date('Y-m-d', $now); if (is_null($this->consumer->last_access)) { $doSaveConsumer = TRUE; } else { $last = date('Y-m-d', $this->consumer->last_access); $doSaveConsumer = $doSaveConsumer || $last != $today; } $this->consumer->last_access = $now; try { $store = new LTI_OAuthDataStore($this); $server = new OAuthServer($store); $method = new OAuthSignatureMethod_HMAC_SHA1(); $server->add_signature_method($method); $request = OAuthRequest::from_request(); $res = $server->verify_request($request); } catch (Exception $e) { $this->isOK = FALSE; if (empty($this->reason)) { if ($this->debugMode) { $consumer = new OAuthConsumer($this->consumer->getKey(), $this->consumer->secret); $signature = $request->build_signature($method, $consumer, FALSE); $this->reason = $e->getMessage(); if (empty($this->reason)) { $this->reason = 'OAuth exception'; } $this->details[] = 'Timestamp: ' . time(); $this->details[] = "Signature: {$signature}"; $this->details[] = "Base string: {$request->base_string}]"; } else { $this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.'; } } } } if ($this->isOK && $this->consumer->protected) { if (!is_null($this->consumer->consumer_guid)) { $this->isOK = isset($_POST['tool_consumer_instance_guid']) && !empty($_POST['tool_consumer_instance_guid']) && $this->consumer->consumer_guid == $_POST['tool_consumer_instance_guid']; if (!$this->isOK) { $this->reason = 'Request is from an invalid tool consumer.'; } } else { $this->isOK = isset($_POST['tool_consumer_instance_guid']); if (!$this->isOK) { $this->reason = 'A tool consumer GUID must be included in the launch request.'; } } } if ($this->isOK) { $this->isOK = $this->consumer->enabled; if (!$this->isOK) { $this->reason = 'Tool consumer has not been enabled by the tool provider.'; } } if ($this->isOK) { $this->isOK = is_null($this->consumer->enable_from) || $this->consumer->enable_from <= $now; if ($this->isOK) { $this->isOK = is_null($this->consumer->enable_until) || $this->consumer->enable_until > $now; if (!$this->isOK) { $this->reason = 'Tool consumer access has expired.'; } } else { $this->reason = 'Tool consumer access is not yet available.'; } } # ### Validate other message parameter values # if ($this->isOK) { if ($_POST['lti_message_type'] != 'ContentItemSelectionRequest') { if (isset($_POST['launch_presentation_document_target'])) { $this->isOK = $this->checkValue($_POST['launch_presentation_document_target'], array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay'), 'Invalid value for launch_presentation_document_target parameter: %s.'); } } else { if (isset($_POST['accept_unsigned'])) { $this->isOK = $this->checkValue($_POST['accept_unsigned'], array('true', 'false'), 'Invalid value for accept_unsigned parameter: %s.'); } if ($this->isOK && isset($_POST['accept_multiple'])) { $this->isOK = $this->checkValue($_POST['accept_multiple'], array('true', 'false'), 'Invalid value for accept_multiple parameter: %s.'); } if ($this->isOK && isset($_POST['accept_copy_advice'])) { $this->isOK = $this->checkValue($_POST['accept_copy_advice'], array('true', 'false'), 'Invalid value for accept_copy_advice parameter: %s.'); } if ($this->isOK && isset($_POST['auto_create'])) { $this->isOK = $this->checkValue($_POST['auto_create'], array('true', 'false'), 'Invalid value for auto_create parameter: %s.'); } if ($this->isOK && isset($_POST['can_confirm'])) { $this->isOK = $this->checkValue($_POST['can_confirm'], array('true', 'false'), 'Invalid value for can_confirm parameter: %s.'); } } } # ### Validate message parameter constraints # if ($this->isOK) { $invalid_parameters = array(); foreach ($this->constraints as $name => $constraint) { if (empty($constraint['messages']) || in_array($_POST['lti_message_type'], $constraint['messages'])) { $ok = TRUE; if ($constraint['required']) { if (!isset($_POST[$name]) || strlen(trim($_POST[$name])) <= 0) { $invalid_parameters[] = "{$name} (missing)"; $ok = FALSE; } } if ($ok && !is_null($constraint['max_length']) && isset($_POST[$name])) { if (strlen(trim($_POST[$name])) > $constraint['max_length']) { $invalid_parameters[] = "{$name} (too long)"; } } } } if (count($invalid_parameters) > 0) { $this->isOK = FALSE; if (empty($this->reason)) { $this->reason = 'Invalid parameter(s): ' . implode(', ', $invalid_parameters) . '.'; } } } if ($this->isOK) { # ### Set the request context/resource link # if (isset($_POST['resource_link_id'])) { $content_item_id = ''; if (isset($_POST['custom_content_item_id'])) { $content_item_id = $_POST['custom_content_item_id']; } $this->resource_link = new LTI_Resource_Link($this->consumer, trim($_POST['resource_link_id']), $content_item_id); if (isset($_POST['context_id'])) { $this->resource_link->lti_context_id = trim($_POST['context_id']); } $this->resource_link->lti_resource_id = trim($_POST['resource_link_id']); $title = ''; if (isset($_POST['context_title'])) { $title = trim($_POST['context_title']); } if (isset($_POST['resource_link_title']) && strlen(trim($_POST['resource_link_title'])) > 0) { if (!empty($title)) { $title .= ': '; } $title .= trim($_POST['resource_link_title']); } if (empty($title)) { $title = "Course {$this->resource_link->getId()}"; } $this->resource_link->title = $title; // Save LTI parameters foreach ($this->lti_settings_names as $name) { if (isset($_POST[$name])) { $this->resource_link->setSetting($name, $_POST[$name]); } else { $this->resource_link->setSetting($name, NULL); } } // Delete any existing custom parameters foreach ($this->resource_link->getSettings() as $name => $value) { if (strpos($name, 'custom_') === 0) { $this->resource_link->setSetting($name); } } // Save custom parameters foreach ($_POST as $name => $value) { if (strpos($name, 'custom_') === 0) { $this->resource_link->setSetting($name, $value); } } } # ### Set the user instance # $user_id = ''; if (isset($_POST['user_id'])) { $user_id = trim($_POST['user_id']); } $this->user = new LTI_User($this->resource_link, $user_id); # ### Set the user name # $firstname = isset($_POST['lis_person_name_given']) ? $_POST['lis_person_name_given'] : ''; $lastname = isset($_POST['lis_person_name_family']) ? $_POST['lis_person_name_family'] : ''; $fullname = isset($_POST['lis_person_name_full']) ? $_POST['lis_person_name_full'] : ''; $this->user->setNames($firstname, $lastname, $fullname); # ### Set the user email # $email = isset($_POST['lis_person_contact_email_primary']) ? $_POST['lis_person_contact_email_primary'] : ''; $this->user->setEmail($email, $this->defaultEmail); # ### Set the user roles # if (isset($_POST['roles'])) { $this->user->roles = LTI_Tool_Provider::parseRoles($_POST['roles']); } # ### Save the user instance # if (isset($_POST['lis_result_sourcedid'])) { if ($this->user->lti_result_sourcedid != $_POST['lis_result_sourcedid']) { $this->user->lti_result_sourcedid = $_POST['lis_result_sourcedid']; $this->user->save(); } } else { if (!empty($this->user->lti_result_sourcedid)) { $this->user->delete(); } } # ### Initialise the consumer and check for changes # $this->consumer->defaultEmail = $this->defaultEmail; if ($this->consumer->lti_version != $_POST['lti_version']) { $this->consumer->lti_version = $_POST['lti_version']; $doSaveConsumer = TRUE; } if (isset($_POST['tool_consumer_instance_name'])) { if ($this->consumer->consumer_name != $_POST['tool_consumer_instance_name']) { $this->consumer->consumer_name = $_POST['tool_consumer_instance_name']; $doSaveConsumer = TRUE; } } if (isset($_POST['tool_consumer_info_product_family_code'])) { $version = $_POST['tool_consumer_info_product_family_code']; if (isset($_POST['tool_consumer_info_version'])) { $version .= "-{$_POST['tool_consumer_info_version']}"; } // do not delete any existing consumer version if none is passed if ($this->consumer->consumer_version != $version) { $this->consumer->consumer_version = $version; $doSaveConsumer = TRUE; } } else { if (isset($_POST['ext_lms']) && $this->consumer->consumer_name != $_POST['ext_lms']) { $this->consumer->consumer_version = $_POST['ext_lms']; $doSaveConsumer = TRUE; } } if (isset($_POST['tool_consumer_instance_guid'])) { if (is_null($this->consumer->consumer_guid)) { $this->consumer->consumer_guid = $_POST['tool_consumer_instance_guid']; $doSaveConsumer = TRUE; } else { if (!$this->consumer->protected) { $doSaveConsumer = $this->consumer->consumer_guid != $_POST['tool_consumer_instance_guid']; if ($doSaveConsumer) { $this->consumer->consumer_guid = $_POST['tool_consumer_instance_guid']; } } } } if (isset($_POST['launch_presentation_css_url'])) { if ($this->consumer->css_path != $_POST['launch_presentation_css_url']) { $this->consumer->css_path = $_POST['launch_presentation_css_url']; $doSaveConsumer = TRUE; } } else { if (isset($_POST['ext_launch_presentation_css_url']) && $this->consumer->css_path != $_POST['ext_launch_presentation_css_url']) { $this->consumer->css_path = $_POST['ext_launch_presentation_css_url']; $doSaveConsumer = TRUE; } else { if (!empty($this->consumer->css_path)) { $this->consumer->css_path = NULL; $doSaveConsumer = TRUE; } } } } # ### Persist changes to consumer # if ($doSaveConsumer) { $this->consumer->save(); } if ($this->isOK && isset($this->resource_link)) { # ### Check if a share arrangement is in place for this resource link # $this->isOK = $this->checkForShare(); # ### Persist changes to resource link # $this->resource_link->save(); } return $this->isOK; }