/** * Perform a Memberships service request. * * The user table is updated with the new list of user objects. * * @param boolean $withGroups True is group information is to be requested as well * * @return mixed Array of User objects or False if the request was not successful */ public function doMembershipsService($withGroups = false) { $users = []; $old_users = $this->getUserResultSourcedIDs(true, ToolProvider::ID_SCOPE_RESOURCE); $url = $this->getSetting('ext_ims_lis_memberships_url'); $params = []; $params['id'] = $this->getSetting('ext_ims_lis_memberships_id'); $ok = false; if ($withGroups) { $ok = $this->doService('basic-lis-readmembershipsforcontextwithgroups', $url, $params); } if ($ok) { $this->groupSets = []; $this->groups = []; } else { $ok = $this->doService('basic-lis-readmembershipsforcontext', $url, $params); } if ($ok) { if (!isset($this->extNodes['memberships']['member'])) { $members = []; } else { if (!isset($this->extNodes['memberships']['member'][0])) { $members = []; $members[0] = $this->extNodes['memberships']['member']; } else { $members = $this->extNodes['memberships']['member']; } } for ($i = 0; $i < count($members); $i++) { $user = new User($this, $members[$i]['user_id']); // Set the user name $firstname = isset($members[$i]['person_name_given']) ? $members[$i]['person_name_given'] : ''; $lastname = isset($members[$i]['person_name_family']) ? $members[$i]['person_name_family'] : ''; $fullname = isset($members[$i]['person_name_full']) ? $members[$i]['person_name_full'] : ''; $user->setNames($firstname, $lastname, $fullname); // Set the user email $email = isset($members[$i]['person_contact_email_primary']) ? $members[$i]['person_contact_email_primary'] : ''; $user->setEmail($email, $this->consumer->defaultEmail); // Set the user roles if (isset($members[$i]['roles'])) { $user->roles = ToolProvider::parseRoles($members[$i]['roles']); } // Set the user groups if (!isset($members[$i]['groups']['group'])) { $groups = []; } else { if (!isset($members[$i]['groups']['group'][0])) { $groups = []; $groups[0] = $members[$i]['groups']['group']; } else { $groups = $members[$i]['groups']['group']; } } for ($j = 0; $j < count($groups); $j++) { $group = $groups[$j]; if (isset($group['set'])) { $set_id = $group['set']['id']; if (!isset($this->groupSets[$set_id])) { $this->groupSets[$set_id] = ['title' => $group['set']['title'], 'groups' => [], 'num_members' => 0, 'num_staff' => 0, 'num_learners' => 0]; } $this->groupSets[$set_id]['num_members']++; if ($user->isStaff()) { $this->groupSets[$set_id]['num_staff']++; } if ($user->isLearner()) { $this->groupSets[$set_id]['num_learners']++; } if (!in_array($group['id'], $this->groupSets[$set_id]['groups'])) { $this->groupSets[$set_id]['groups'][] = $group['id']; } $this->groups[$group['id']] = ['title' => $group['title'], 'set' => $set_id]; } else { $this->groups[$group['id']] = ['title' => $group['title']]; } $user->groups[] = $group['id']; } // If a result sourcedid is provided save the user if (isset($members[$i]['lis_result_sourcedid'])) { $user->ltiResultSourcedId = $members[$i]['lis_result_sourcedid']; $user->save(); } $users[] = $user; // Remove old user (if it exists) unset($old_users[$user->getId(ToolProvider::ID_SCOPE_RESOURCE)]); } // Delete any old users which were not in the latest list from the tool consumer foreach ($old_users as $id => $user) { $user->delete(); } } else { $users = false; } return $users; }
/** * Check the authenticity of the LTI launch request. * * The consumer, resource link and user objects will be initialised if the request is valid. * * @param ServerRequestInterface $request * @return bool True if the request has been successfully validated. * @throws Exception * @internal param array $requestBody */ private function authenticate(ServerRequestInterface $request) { $requestBody = (array) $request->getParsedBody(); // Get the consumer $doSaveConsumer = false; // Check all required launch parameters $version = isset($requestBody['lti_version']) ? $requestBody['lti_version'] : ''; $messageType = isset($requestBody['lti_message_type']) ? $requestBody['lti_message_type'] : ''; if (!in_array($version, $this->LTI_VERSIONS)) { throw new Exception('Invalid or missing lti_version parameter'); } switch ($messageType) { case 'basic-lti-launch-request': case 'DashboardRequest': if (!isset($requestBody['resource_link_id']) || strlen(trim($requestBody['resource_link_id'])) == 0) { throw new Exception('Missing resource link ID'); } break; case 'ContentItemSelectionRequest': $acceptMediaTypes = isset($requestBody['accept_media_types']) ? trim($requestBody['accept_media_types']) : ''; if (strlen($acceptMediaTypes) == 0) { throw new Exception('No accept_media_types found'); } $mediaTypes = array_filter(explode(',', str_replace(' ', '', $acceptMediaTypes)), 'strlen'); $mediaTypes = array_unique($mediaTypes); if (count($mediaTypes) == 0) { throw new Exception('No valid accept_media_types found'); } $this->mediaTypes = $mediaTypes; $acceptDocumentTargets = isset($requestBody['accept_presentation_document_targets']) ? trim($requestBody['accept_presentation_document_targets']) : ''; if (strlen($acceptDocumentTargets) == 0) { throw new Exception('No accept_presentation_document_targets found'); } $documentTargets = array_filter(explode(',', str_replace(' ', '', $acceptDocumentTargets), 'strlen')); $documentTargets = array_unique($documentTargets); if (count($documentTargets) == 0) { throw new Exception('No valid accept_presentation_document_targets found'); } foreach ($documentTargets as $documentTarget) { $this->checkValue($documentTarget, ['embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'], 'Invalid value in accept_presentation_document_targets parameter: %s.'); } $this->documentTargets = $documentTargets; $returnUrl = isset($requestBody['content_item_return_url']) ? trim($requestBody['content_item_return_url']) : ''; if (strlen($returnUrl) == 0) { throw new Exception('Missing content_item_return_url parameter'); } break; default: throw new Exception('Invalid or missing lti_message_type parameter'); } // Check consumer key if (!isset($requestBody['oauth_consumer_key'])) { throw new Exception('Missing consumer key'); } $this->consumer = new ToolConsumer($requestBody['oauth_consumer_key'], $this->storage); if (is_null($this->consumer->created)) { throw new Exception('Invalid consumer key'); } $now = time(); $today = date('Y-m-d', $now); if (is_null($this->consumer->lastAccess)) { $doSaveConsumer = true; } else { $last = date('Y-m-d', $this->consumer->lastAccess); $doSaveConsumer = $doSaveConsumer || $last != $today; } $this->consumer->lastAccess = $now; $store = new DataStore($this); $server = new Server($store); $method = new SignatureMethodHmacSha1(); $server->addSignatureMethod($method); $request = Request::fromPsrRequest($request); $server->verifyRequest($request); if ($this->consumer->protected) { $consumerGuid = isset($requestBody['tool_consumer_instance_guid']) ? $requestBody['tool_consumer_instance_guid'] : ''; if (empty($consumerGuid)) { throw new Exception('A tool consumer GUID must be included in the launch request'); } if ($this->consumer->consumerGuid !== $consumerGuid) { throw new Exception('Request is from an invalid tool consumer'); } } if (!$this->consumer->enabled) { throw new Exception('Tool consumer has not been enabled by the tool provider'); } if (!is_null($this->consumer->enableFrom) && $this->consumer->enableFrom > $now) { throw new Exception('Tool consumer access is not yet available'); } if (!is_null($this->consumer->enableUntil) && $this->consumer->enableUntil <= $now) { throw new Exception('Tool consumer access has expired'); } // Validate other message parameter values if ($requestBody['lti_message_type'] != 'ContentItemSelectionRequest') { if (isset($requestBody['launch_presentation_document_target'])) { $this->checkValue($requestBody['launch_presentation_document_target'], ['embed', 'frame', 'iframe', 'window', 'popup', 'overlay'], 'Invalid value for launch_presentation_document_target parameter: %s.'); } } else { if (isset($requestBody['accept_unsigned'])) { $this->checkValue($requestBody['accept_unsigned'], ['true', 'false'], 'Invalid value for accept_unsigned parameter: %s.'); } if (isset($requestBody['accept_multiple'])) { $this->checkValue($requestBody['accept_multiple'], ['true', 'false'], 'Invalid value for accept_multiple parameter: %s.'); } if (isset($requestBody['accept_copy_advice'])) { $this->checkValue($requestBody['accept_copy_advice'], ['true', 'false'], 'Invalid value for accept_copy_advice parameter: %s.'); } if (isset($requestBody['auto_create'])) { $this->checkValue($requestBody['auto_create'], ['true', 'false'], 'Invalid value for auto_create parameter: %s.'); } if (isset($requestBody['can_confirm'])) { $this->checkValue($requestBody['can_confirm'], ['true', 'false'], 'Invalid value for can_confirm parameter: %s.'); } } // Validate message parameter constraints $invalid_parameters = []; foreach ($this->constraints as $name => $constraint) { if (empty($constraint['messages']) || in_array($messageType, $constraint['messages'])) { $ok = true; if ($constraint['required']) { if (!isset($requestBody[$name]) || strlen(trim($requestBody[$name])) <= 0) { $invalid_parameters[] = "{$name} (missing)"; $ok = false; } } if ($ok && !is_null($constraint['max_length']) && isset($requestBody[$name])) { if (strlen(trim($requestBody[$name])) > $constraint['max_length']) { $invalid_parameters[] = "{$name} (too long)"; } } } } if (count($invalid_parameters) > 0) { throw new Exception('Invalid parameter(s): ' . implode(', ', $invalid_parameters)); } // Set the request context/resource link if (isset($requestBody['resource_link_id'])) { $content_item_id = ''; if (isset($requestBody['custom_content_item_id'])) { $content_item_id = $requestBody['custom_content_item_id']; } $this->resourceLink = new ResourceLink($this->consumer, trim($requestBody['resource_link_id']), $content_item_id); if (isset($requestBody['context_id'])) { $this->resourceLink->lti_context_id = trim($requestBody['context_id']); } $this->resourceLink->lti_resource_id = trim($requestBody['resource_link_id']); $title = ''; if (isset($requestBody['context_title'])) { $title = trim($requestBody['context_title']); } if (isset($requestBody['resource_link_title']) && strlen(trim($requestBody['resource_link_title'])) > 0) { if (!empty($title)) { $title .= ': '; } $title .= trim($requestBody['resource_link_title']); } if (empty($title)) { $title = "Course {$this->resourceLink->getId()}"; } $this->resourceLink->title = $title; // Save LTI parameters foreach ($this->ltiSettingsNames as $name) { if (isset($requestBody[$name])) { $this->resourceLink->setSetting($name, $requestBody[$name]); } else { $this->resourceLink->setSetting($name, null); } } // Delete any existing custom parameters foreach ($this->resourceLink->getSettings() as $name => $value) { if (strpos($name, 'custom_') === 0) { $this->resourceLink->setSetting($name); } } // Save custom parameters foreach ($requestBody as $name => $value) { if (strpos($name, 'custom_') === 0) { $this->resourceLink->setSetting($name, $value); } } } // Set the user instance $user_id = ''; if (isset($requestBody['user_id'])) { $user_id = trim($requestBody['user_id']); } $this->user = new User($this->resourceLink, $user_id); // Set the user name if (isset($requestBody['lis_person_name_given'])) { $firstname = $requestBody['lis_person_name_given']; } elseif (isset($requestBody['custom_lis_person_given_name'])) { $firstname = $requestBody['custom_lis_person_given_name']; } else { $firstname = ''; } if (isset($requestBody['lis_person_name_family'])) { $lastname = $requestBody['lis_person_name_family']; } elseif (isset($requestBody['custom_lis_person_name_family'])) { $lastname = $requestBody['custom_lis_person_name_family']; } else { $lastname = ''; } if (isset($requestBody['lis_person_name_full'])) { $fullname = $requestBody['lis_person_name_full']; } elseif (isset($requestBody['custom_lis_person_name_full'])) { $fullname = $requestBody['custom_lis_person_name_full']; } else { $fullname = ''; } $this->user->setNames($firstname, $lastname, $fullname); // Set the user email if (isset($requestBody['lis_person_contact_email_primary'])) { $email = $requestBody['lis_person_contact_email_primary']; } elseif (isset($requestBody['custom_lis_person_contact_email_primary'])) { $email = $requestBody['custom_lis_person_contact_email_primary']; } else { $email = ''; } $this->user->setEmail($email, $this->defaultEmail); // Set the user roles if (isset($requestBody['roles'])) { $this->user->roles = ToolProvider::parseRoles($requestBody['roles']); } // Save the user instance if (isset($requestBody['lis_result_sourcedid'])) { if ($this->user->ltiResultSourcedId != $requestBody['lis_result_sourcedid']) { $this->user->ltiResultSourcedId = $requestBody['lis_result_sourcedid']; $this->user->save(); } } else { if (!empty($this->user->ltiResultSourcedId)) { $this->user->delete(); } } // Initialise the consumer and check for changes $this->consumer->defaultEmail = $this->defaultEmail; if ($this->consumer->ltiVersion != $requestBody['lti_version']) { $this->consumer->ltiVersion = $requestBody['lti_version']; $doSaveConsumer = true; } if (isset($requestBody['tool_consumer_instance_name'])) { if ($this->consumer->consumerName != $requestBody['tool_consumer_instance_name']) { $this->consumer->consumerName = $requestBody['tool_consumer_instance_name']; $doSaveConsumer = true; } } if (isset($requestBody['tool_consumer_info_product_family_code'])) { $version = $requestBody['tool_consumer_info_product_family_code']; if (isset($requestBody['tool_consumer_info_version'])) { $version .= "-{$requestBody['tool_consumer_info_version']}"; } // do not delete any existing consumer version if none is passed if ($this->consumer->consumerVersion != $version) { $this->consumer->consumerVersion = $version; $doSaveConsumer = true; } } else { if (isset($requestBody['ext_lms']) && $this->consumer->consumerName != $requestBody['ext_lms']) { $this->consumer->consumerVersion = $requestBody['ext_lms']; $doSaveConsumer = true; } } if (isset($requestBody['tool_consumer_instance_guid'])) { if (is_null($this->consumer->consumerGuid)) { $this->consumer->consumerGuid = $requestBody['tool_consumer_instance_guid']; $doSaveConsumer = true; } else { if (!$this->consumer->protected) { $doSaveConsumer = $this->consumer->consumerGuid != $requestBody['tool_consumer_instance_guid']; if ($doSaveConsumer) { $this->consumer->consumerGuid = $requestBody['tool_consumer_instance_guid']; } } } } if (isset($requestBody['launch_presentation_css_url'])) { if ($this->consumer->cssPath != $requestBody['launch_presentation_css_url']) { $this->consumer->cssPath = $requestBody['launch_presentation_css_url']; $doSaveConsumer = true; } } else { if (isset($requestBody['ext_launch_presentation_css_url']) && $this->consumer->cssPath != $requestBody['ext_launch_presentation_css_url']) { $this->consumer->cssPath = $requestBody['ext_launch_presentation_css_url']; $doSaveConsumer = true; } else { if (!empty($this->consumer->cssPath)) { $this->consumer->cssPath = null; $doSaveConsumer = true; } } } // Persist changes to consumer if ($doSaveConsumer) { $this->consumer->save(); } if (isset($this->resourceLink)) { // Check if a share arrangement is in place for this resource link $this->checkForShare($requestBody); // Persist changes to resource link $this->resourceLink->save(); } return true; }