AutorÃa | Ultima modificación | Ver Log |
<?phpnamespace IMSGlobal\LTI\ToolProvider;use IMSGlobal\LTI\Profile\Item;use IMSGlobal\LTI\ToolProvider\DataConnector\DataConnector;use IMSGlobal\LTI\ToolProvider\MediaType;use IMSGlobal\LTI\Profile;use IMSGlobal\LTI\HTTPMessage;use IMSGlobal\LTI\OAuth;/*** 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>)*/#[\AllowDynamicProperties]class ToolProvider{/*** Default connection error message.*/const CONNECTION_ERROR_MESSAGE = 'Sorry, there was an error connecting you to the application.';/*** LTI version 1 for messages.*/const LTI_VERSION1 = 'LTI-1p0';/*** LTI version 2 for messages.*/const LTI_VERSION2 = 'LTI-2p0';/*** Use ID value only.*/const ID_SCOPE_ID_ONLY = 0;/*** Prefix an ID with the consumer key.*/const ID_SCOPE_GLOBAL = 1;/*** Prefix the ID with the consumer key and context ID.*/const ID_SCOPE_CONTEXT = 2;/*** Prefix the ID with the consumer key and resource ID.*/const ID_SCOPE_RESOURCE = 3;/*** Character used to separate each element of an ID.*/const ID_SCOPE_SEPARATOR = ':';/*** Permitted LTI versions for messages.*/private static $LTI_VERSIONS = array(self::LTI_VERSION1, self::LTI_VERSION2);/*** List of supported message types and associated class methods.*/private static $MESSAGE_TYPES = array('basic-lti-launch-request' => 'onLaunch','ContentItemSelectionRequest' => 'onContentItem','ToolProxyRegistrationRequest' => 'register');/*** List of supported message types and associated class methods** @var array $METHOD_NAMES*/private static $METHOD_NAMES = array('basic-lti-launch-request' => 'onLaunch','ContentItemSelectionRequest' => 'onContentItem','ToolProxyRegistrationRequest' => 'onRegister');/*** Names of LTI parameters to be retained in the consumer settings property.** @var array $LTI_CONSUMER_SETTING_NAMES*/private static $LTI_CONSUMER_SETTING_NAMES = array('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 $LTI_CONTEXT_SETTING_NAMES = array('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 $LTI_RESOURCE_LINK_SETTING_NAMES = array('lis_course_section_sourcedid', '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 $CUSTOM_SUBSTITUTION_VARIABLES = array('User.id' => 'user_id','User.image' => 'user_image','User.username' => 'username','User.scope.mentor' => 'role_scope_mentor','Membership.role' => 'roles','Person.sourcedId' => 'lis_person_sourcedid','Person.name.full' => 'lis_person_name_full','Person.name.family' => 'lis_person_name_family','Person.name.given' => 'lis_person_name_given','Person.email.primary' => 'lis_person_contact_email_primary','Context.id' => 'context_id','Context.type' => 'context_type','Context.title' => 'context_title','Context.label' => 'context_label','CourseOffering.sourcedId' => 'lis_course_offering_sourcedid','CourseSection.sourcedId' => 'lis_course_section_sourcedid','CourseSection.label' => 'context_label','CourseSection.title' => 'context_title','ResourceLink.id' => 'resource_link_id','ResourceLink.title' => 'resource_link_title','ResourceLink.description' => 'resource_link_description','Result.sourcedId' => 'lis_result_sourcedid','BasicOutcome.url' => 'lis_outcome_service_url','ToolConsumerProfile.url' => 'custom_tc_profile_url','ToolProxy.url' => 'tool_proxy_url','ToolProxy.custom.url' => 'custom_system_setting_url','ToolProxyBinding.custom.url' => 'custom_context_setting_url','LtiLink.custom.url' => 'custom_link_setting_url','LineItems.url' => 'custom_lineitems_url','LineItem.url' => 'custom_lineitem_url','Results.url' => 'custom_results_url','Result.url' => 'custom_result_url','ToolProxyBinding.memberships.url' => 'custom_context_memberships_url');/*** True if the last request was successful.** @var boolean $ok*/public $ok = true;/*** Tool Consumer object.** @var ToolConsumer $consumer*/public $consumer = null;/*** Return URL provided by tool consumer.** @var string $returnUrl*/public $returnUrl = null;/*** User object.** @var User $user*/public $user = null;/*** Resource link object.** @var ResourceLink $resourceLink*/public $resourceLink = null;/*** Context object.** @var Context $context*/public $context = null;/*** Data connector object.** @var DataConnector $dataConnector*/public $dataConnector = null;/*** Default email domain.** @var string $defaultEmail*/public $defaultEmail = '';/*** Scope to use for user IDs.** @var int $idScope*/public $idScope = self::ID_SCOPE_ID_ONLY;/*** Whether shared resource link arrangements are permitted.** @var boolean $allowSharing*/public $allowSharing = false;/*** Message for last request processed** @var string $message*/public $message = self::CONNECTION_ERROR_MESSAGE;/*** Error message for last request processed.** @var string $reason*/public $reason = null;/*** Details for error message relating to last request processed.** @var array $details*/public $details = array();/*** Base URL for tool provider service** @var string $baseUrl*/public $baseUrl = null;/*** Vendor details** @var Item $vendor*/public $vendor = null;/*** Product details** @var Item $product*/public $product = null;/*** Services required by Tool Provider** @var array $requiredServices*/public $requiredServices = null;/*** Optional services used by Tool Provider** @var array $optionalServices*/public $optionalServices = null;/*** Resource handlers for Tool Provider** @var array $resourceHandlers*/public $resourceHandlers = null;/*** URL to redirect user to on successful completion of the request.** @var string $redirectUrl*/protected $redirectUrl = null;/*** URL to redirect user to on successful completion of the request.** @var string $mediaTypes*/protected $mediaTypes = null;/*** URL to redirect user to on successful completion of the request.** @var string $documentTargets*/protected $documentTargets = null;/*** HTML to be displayed on a successful completion of the request.** @var string $output*/protected $output = null;/*** HTML to be displayed on an unsuccessful completion of the request and no return URL is available.** @var string $errorOutput*/protected $errorOutput = null;/*** Whether debug messages explaining the cause of errors are to be returned to the tool consumer.** @var boolean $debugMode*/protected $debugMode = false;/*** Callback functions for handling requests.** @var array $callbackHandler*/private $callbackHandler = null;/*** LTI parameter constraints for auto validation checks.** @var array $constraints*/private $constraints = null;/*** Class constructor** @param DataConnector $dataConnector Object containing a database connection object*/function __construct($dataConnector){$this->constraints = array();$this->dataConnector = $dataConnector;$this->ok = !is_null($this->dataConnector);// Set debug mode$this->debugMode = isset($_POST['custom_debug']) && (strtolower($_POST['custom_debug']) === 'true');// Set return URL if availableif (isset($_POST['launch_presentation_return_url'])) {$this->returnUrl = $_POST['launch_presentation_return_url'];} else if (isset($_POST['content_item_return_url'])) {$this->returnUrl = $_POST['content_item_return_url'];}$this->vendor = new Profile\Item();$this->product = new Profile\Item();$this->requiredServices = array();$this->optionalServices = array();$this->resourceHandlers = array();}/*** Process an incoming request*/public function handleRequest(){if ($this->ok) {if ($this->authenticate()) {$this->doCallback();}}$this->result();}/*** 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 function setParameterConstraint($name, $required = true, $maxLength = null, $messageTypes = null){$name = trim($name);if (strlen($name) > 0) {$this->constraints[$name] = array('required' => $required, 'max_length' => $maxLength, 'messages' => $messageTypes);}}/*** Get an array of defined tool consumers** @return array Array of ToolConsumer objects*/public function getConsumers(){return $this->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 function findService($format, $methods){$found = false;$services = $this->consumer->profile->service_offered;if (is_array($services)) {$n = -1;foreach ($services as $service) {$n++;if (!is_array($service->format) || !in_array($format, $service->format)) {continue;}$missing = array();foreach ($methods as $method) {if (!is_array($service->action) || !in_array($method, $service->action)) {$missing[] = $method;}}$methods = $missing;if (count($methods) <= 0) {$found = $service;break;}}}return $found;}/*** Send the tool proxy to the Tool Consumer** @return boolean True if the tool proxy was accepted*/public function doToolProxyService(){// Create tool proxy$toolProxyService = $this->findService('application/vnd.ims.lti.v2.toolproxy+json', array('POST'));$secret = DataConnector::getRandomString(12);$toolProxy = new MediaType\ToolProxy($this, $toolProxyService, $secret);$http = $this->consumer->doServiceRequest($toolProxyService, 'POST', 'application/vnd.ims.lti.v2.toolproxy+json', json_encode($toolProxy));$ok = $http->ok && ($http->status == 201) && isset($http->responseJson->tool_proxy_guid) && (strlen($http->responseJson->tool_proxy_guid) > 0);if ($ok) {$this->consumer->setKey($http->responseJson->tool_proxy_guid);$this->consumer->secret = $toolProxy->security_contract->shared_secret;$this->consumer->toolProxy = json_encode($toolProxy);$this->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 function parseRoles($roles){if (!is_array($roles)) {$roles = explode(',', $roles);}$parsedRoles = array();foreach ($roles as $role) {$role = trim($role);if (!empty($role)) {if (substr($role, 0, 4) !== 'urn:') {$role = 'urn:lti:role:ims/lis/' . $role;}$parsedRoles[] = $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 function sendForm($url, $params, $target = ''){$page = <<< EOD<html><head><title>IMS LTI message</title><script type="text/javascript">//<![CDATA[function doOnLoad() {document.forms[0].submit();}window.onload=doOnLoad;//]]></script></head><body><form action="{$url}" method="post" target="" encType="application/x-www-form-urlencoded">EOD;foreach($params as $key => $value ) {$key = htmlentities($key, ENT_COMPAT | ENT_HTML401, 'UTF-8');$value = htmlentities($value, ENT_COMPAT | ENT_HTML401, 'UTF-8');$page .= <<< EOD<input type="hidden" name="{$key}" value="{$value}" />EOD;}$page .= <<< EOD</form></body></html>EOD;return $page;}###### PROTECTED METHODS###/*** Process a valid launch request** @return boolean True if no error*/protected function onLaunch(){$this->onError();}/*** Process a valid content-item request** @return boolean True if no error*/protected function onContentItem(){$this->onError();}/*** Process a valid tool proxy registration request** @return boolean True if no error*/protected function onRegister() {$this->onError();}/*** Process a response to an invalid request** @return boolean True if no further error processing required*/protected function onError(){$this->doCallback('onError');}###### 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 function doCallback($method = null){$callback = $method;if (is_null($callback)) {$callback = self::$METHOD_NAMES[$_POST['lti_message_type']];}if (method_exists($this, $callback)) {$result = $this->$callback();} else if (is_null($method) && $this->ok) {$this->ok = false;$this->reason = "Message type not supported: {$_POST['lti_message_type']}";}if ($this->ok && ($_POST['lti_message_type'] == 'ToolProxyRegistrationRequest')) {$this->consumer->save();}}/*** 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 function result(){$ok = false;if (!$this->ok) {$ok = $this->onError();}if (!$ok) {if (!$this->ok) {// If not valid, return an error message to the tool consumer if a return URL is providedif (!empty($this->returnUrl)) {$errorUrl = $this->returnUrl;if (strpos($errorUrl, '?') === false) {$errorUrl .= '?';} else {$errorUrl .= '&';}if ($this->debugMode && !is_null($this->reason)) {$errorUrl .= 'lti_errormsg=' . urlencode("Debug error: $this->reason");} else {$errorUrl .= 'lti_errormsg=' . urlencode($this->message);if (!is_null($this->reason)) {$errorUrl .= '<i_errorlog=' . urlencode("Debug error: $this->reason");}}if (!is_null($this->consumer) && isset($_POST['lti_message_type']) && ($_POST['lti_message_type'] === 'ContentItemSelectionRequest')) {$formParams = array();if (isset($_POST['data'])) {$formParams['data'] = $_POST['data'];}$version = (isset($_POST['lti_version'])) ? $_POST['lti_version'] : self::LTI_VERSION1;$formParams = $this->consumer->signParameters($errorUrl, 'ContentItemSelection', $version, $formParams);$page = self::sendForm($errorUrl, $formParams);echo $page;} else {header("Location: {$errorUrl}");}exit;} else {if (!is_null($this->errorOutput)) {echo $this->errorOutput;} else if ($this->debugMode && !empty($this->reason)) {echo "Debug error: {$this->reason}";} else {echo "Error: {$this->message}";}}} else if (!is_null($this->redirectUrl)) {header("Location: {$this->redirectUrl}");exit;} else if (!is_null($this->output)) {echo $this->output;}}}/*** 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->ok = isset($_POST['lti_message_type']) && array_key_exists($_POST['lti_message_type'], self::$MESSAGE_TYPES);if (!$this->ok) {$this->reason = 'Invalid or missing lti_message_type parameter.';}if ($this->ok) {$this->ok = isset($_POST['lti_version']) && in_array($_POST['lti_version'], self::$LTI_VERSIONS);if (!$this->ok) {$this->reason = 'Invalid or missing lti_version parameter.';}}if ($this->ok) {if ($_POST['lti_message_type'] === 'basic-lti-launch-request') {$this->ok = isset($_POST['resource_link_id']) && (strlen(trim($_POST['resource_link_id'])) > 0);if (!$this->ok) {$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->ok = count($mediaTypes) > 0;if (!$this->ok) {$this->reason = 'No accept_media_types found.';} else {$this->mediaTypes = $mediaTypes;}} else {$this->ok = false;}if ($this->ok && 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->ok = count($documentTargets) > 0;if (!$this->ok) {$this->reason = 'Missing or empty accept_presentation_document_targets parameter.';} else {foreach ($documentTargets as $documentTarget) {$this->ok = $this->checkValue($documentTarget, array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'),'Invalid value in accept_presentation_document_targets parameter: %s.');if (!$this->ok) {break;}}if ($this->ok) {$this->documentTargets = $documentTargets;}}} else {$this->ok = false;}if ($this->ok) {$this->ok = isset($_POST['content_item_return_url']) && (strlen(trim($_POST['content_item_return_url'])) > 0);if (!$this->ok) {$this->reason = 'Missing content_item_return_url parameter.';}}} else if ($_POST['lti_message_type'] == 'ToolProxyRegistrationRequest') {$this->ok = ((isset($_POST['reg_key']) && (strlen(trim($_POST['reg_key'])) > 0)) &&(isset($_POST['reg_password']) && (strlen(trim($_POST['reg_password'])) > 0)) &&(isset($_POST['tc_profile_url']) && (strlen(trim($_POST['tc_profile_url'])) > 0)) &&(isset($_POST['launch_presentation_return_url']) && (strlen(trim($_POST['launch_presentation_return_url'])) > 0)));if ($this->debugMode && !$this->ok) {$this->reason = 'Missing message parameters.';}}}$now = time();// Check consumer keyif ($this->ok && ($_POST['lti_message_type'] != 'ToolProxyRegistrationRequest')) {$this->ok = isset($_POST['oauth_consumer_key']);if (!$this->ok) {$this->reason = 'Missing consumer key.';}if ($this->ok) {$this->consumer = new ToolConsumer($_POST['oauth_consumer_key'], $this->dataConnector);$this->ok = !is_null($this->consumer->created);if (!$this->ok) {$this->reason = 'Invalid consumer key.';}}if ($this->ok) {$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->last_access = $now;try {$store = new OAuthDataStore($this);$server = new OAuth\OAuthServer($store);$method = new OAuth\OAuthSignatureMethod_HMAC_SHA1();$server->add_signature_method($method);$request = OAuth\OAuthRequest::from_request();$res = $server->verify_request($request);} catch (\Exception $e) {$this->ok = false;if (empty($this->reason)) {if ($this->debugMode) {$consumer = new OAuth\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->ok) {$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->last_access = $now;if ($this->consumer->protected) {if (!is_null($this->consumer->consumerGuid)) {$this->ok = empty($_POST['tool_consumer_instance_guid']) ||($this->consumer->consumerGuid === $_POST['tool_consumer_instance_guid']);if (!$this->ok) {$this->reason = 'Request is from an invalid tool consumer.';}}}if ($this->ok) {$this->ok = $this->consumer->enabled;if (!$this->ok) {$this->reason = 'Tool consumer has not been enabled by the tool provider.';}}if ($this->ok) {$this->ok = is_null($this->consumer->enableFrom) || ($this->consumer->enableFrom <= $now);if ($this->ok) {$this->ok = is_null($this->consumer->enableUntil) || ($this->consumer->enableUntil > $now);if (!$this->ok) {$this->reason = 'Tool consumer access has expired.';}} else {$this->reason = 'Tool consumer access is not yet available.';}}}// Validate other message parameter valuesif ($this->ok) {if ($_POST['lti_message_type'] === 'ContentItemSelectionRequest') {if (isset($_POST['accept_unsigned'])) {$this->ok = $this->checkValue($_POST['accept_unsigned'], array('true', 'false'), 'Invalid value for accept_unsigned parameter: %s.');}if ($this->ok && isset($_POST['accept_multiple'])) {$this->ok = $this->checkValue($_POST['accept_multiple'], array('true', 'false'), 'Invalid value for accept_multiple parameter: %s.');}if ($this->ok && isset($_POST['accept_copy_advice'])) {$this->ok = $this->checkValue($_POST['accept_copy_advice'], array('true', 'false'), 'Invalid value for accept_copy_advice parameter: %s.');}if ($this->ok && isset($_POST['auto_create'])) {$this->ok = $this->checkValue($_POST['auto_create'], array('true', 'false'), 'Invalid value for auto_create parameter: %s.');}if ($this->ok && isset($_POST['can_confirm'])) {$this->ok = $this->checkValue($_POST['can_confirm'], array('true', 'false'), 'Invalid value for can_confirm parameter: %s.');}} else if (isset($_POST['launch_presentation_document_target'])) {$this->ok = $this->checkValue($_POST['launch_presentation_document_target'], array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay'),'Invalid value for launch_presentation_document_target parameter: %s.');}}}if ($this->ok && ($_POST['lti_message_type'] === 'ToolProxyRegistrationRequest')) {$this->ok = $_POST['lti_version'] == self::LTI_VERSION2;if (!$this->ok) {$this->reason = 'Invalid lti_version parameter';}if ($this->ok) {$http = new HTTPMessage($_POST['tc_profile_url'], 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');$this->ok = $http->send();if (!$this->ok) {$this->reason = 'Tool consumer profile not accessible.';} else {$tcProfile = json_decode($http->response);$this->ok = !is_null($tcProfile);if (!$this->ok) {$this->reason = 'Invalid JSON in tool consumer profile.';}}}// Check for required capabilitiesif ($this->ok) {$this->consumer = new ToolConsumer($_POST['reg_key'], $this->dataConnector);$this->consumer->profile = $tcProfile;$capabilities = $this->consumer->profile->capability_offered;$missing = array();foreach ($this->resourceHandlers as $resourceHandler) {foreach ($resourceHandler->requiredMessages as $message) {if (!in_array($message->type, $capabilities)) {$missing[$message->type] = true;}}}foreach ($this->constraints as $name => $constraint) {if ($constraint['required']) {if (!in_array($name, $capabilities) && !in_array($name, array_flip($capabilities))) {$missing[$name] = true;}}}if (!empty($missing)) {ksort($missing);$this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\'';$this->ok = false;}}// Check for required servicesif ($this->ok) {foreach ($this->requiredServices as $service) {foreach ($service->formats as $format) {if (!$this->findService($format, $service->actions)) {if ($this->ok) {$this->reason = 'Required service(s) not offered - ';$this->ok = false;} else {$this->reason .= ', ';}$this->reason .= "'{$format}' [" . implode(', ', $service->actions) . ']';}}}}if ($this->ok) {if ($_POST['lti_message_type'] === 'ToolProxyRegistrationRequest') {$this->consumer->profile = $tcProfile;$this->consumer->secret = $_POST['reg_password'];$this->consumer->ltiVersion = $_POST['lti_version'];$this->consumer->name = $tcProfile->product_instance->service_owner->service_owner_name->default_value;$this->consumer->consumerName = $this->consumer->name;$this->consumer->consumerVersion = "{$tcProfile->product_instance->product_info->product_family->code}-{$tcProfile->product_instance->product_info->product_version}";$this->consumer->consumerGuid = $tcProfile->product_instance->guid;$this->consumer->enabled = true;$this->consumer->protected = true;$doSaveConsumer = true;}}} else if ($this->ok && !empty($_POST['custom_tc_profile_url']) && empty($this->consumer->profile)) {$http = new HTTPMessage($_POST['custom_tc_profile_url'], 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');if ($http->send()) {$tcProfile = json_decode($http->response);if (!is_null($tcProfile)) {$this->consumer->profile = $tcProfile;$doSaveConsumer = true;}}}// Validate message parameter constraintsif ($this->ok) {$invalidParameters = 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)) {$invalidParameters[] = "{$name} (missing)";$ok = false;}}if ($ok && !is_null($constraint['max_length']) && isset($_POST[$name])) {if (strlen(trim($_POST[$name])) > $constraint['max_length']) {$invalidParameters[] = "{$name} (too long)";}}}}if (count($invalidParameters) > 0) {$this->ok = false;if (empty($this->reason)) {$this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.';}}}if ($this->ok) {// Set the request contextif (isset($_POST['context_id'])) {$this->context = Context::fromConsumer($this->consumer, trim($_POST['context_id']));$title = '';if (isset($_POST['context_title'])) {$title = trim($_POST['context_title']);}if (empty($title)) {$title = "Course {$this->context->getId()}";}if (isset($_POST['context_type'])) {$this->context->type = trim($_POST['context_type']);}$this->context->title = $title;}// Set the request resource linkif (isset($_POST['resource_link_id'])) {$contentItemId = '';if (isset($_POST['custom_content_item_id'])) {$contentItemId = $_POST['custom_content_item_id'];}$this->resourceLink = ResourceLink::fromConsumer($this->consumer, trim($_POST['resource_link_id']), $contentItemId);if (!empty($this->context)) {$this->resourceLink->setContextId($this->context->getRecordId());}$title = '';if (isset($_POST['resource_link_title'])) {$title = trim($_POST['resource_link_title']);}if (empty($title)) {$title = "Resource {$this->resourceLink->getId()}";}$this->resourceLink->title = $title;// Delete any existing custom parametersforeach ($this->consumer->getSettings() as $name => $value) {if (strpos($name, 'custom_') === 0) {$this->consumer->setSetting($name);$doSaveConsumer = true;}}if (!empty($this->context)) {foreach ($this->context->getSettings() as $name => $value) {if (strpos($name, 'custom_') === 0) {$this->context->setSetting($name);}}}foreach ($this->resourceLink->getSettings() as $name => $value) {if (strpos($name, 'custom_') === 0) {$this->resourceLink->setSetting($name);}}// Save LTI parametersforeach (self::$LTI_CONSUMER_SETTING_NAMES as $name) {if (isset($_POST[$name])) {$this->consumer->setSetting($name, $_POST[$name]);} else {$this->consumer->setSetting($name);}}if (!empty($this->context)) {foreach (self::$LTI_CONTEXT_SETTING_NAMES as $name) {if (isset($_POST[$name])) {$this->context->setSetting($name, $_POST[$name]);} else {$this->context->setSetting($name);}}}foreach (self::$LTI_RESOURCE_LINK_SETTING_NAMES as $name) {if (isset($_POST[$name])) {$this->resourceLink->setSetting($name, $_POST[$name]);} else {$this->resourceLink->setSetting($name);}}// Save other custom parametersforeach ($_POST as $name => $value) {if ((strpos($name, 'custom_') === 0) &&!in_array($name, array_merge(self::$LTI_CONSUMER_SETTING_NAMES, self::$LTI_CONTEXT_SETTING_NAMES, self::$LTI_RESOURCE_LINK_SETTING_NAMES))) {$this->resourceLink->setSetting($name, $value);}}}// Set the user instance$userId = '';if (isset($_POST['user_id'])) {$userId = trim($_POST['user_id']);}$this->user = User::fromResourceLink($this->resourceLink, $userId);// 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 image URIif (isset($_POST['user_image'])) {$this->user->image = $_POST['user_image'];}// Set the user rolesif (isset($_POST['roles'])) {$this->user->roles = self::parseRoles($_POST['roles']);}// Initialise the consumer and check for changes$this->consumer->defaultEmail = $this->defaultEmail;if ($this->consumer->ltiVersion !== $_POST['lti_version']) {$this->consumer->ltiVersion = $_POST['lti_version'];$doSaveConsumer = true;}if (isset($_POST['tool_consumer_instance_name'])) {if ($this->consumer->consumerName !== $_POST['tool_consumer_instance_name']) {$this->consumer->consumerName = $_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 passedif ($this->consumer->consumerVersion !== $version) {$this->consumer->consumerVersion = $version;$doSaveConsumer = true;}} else if (isset($_POST['ext_lms']) && ($this->consumer->consumerName !== $_POST['ext_lms'])) {$this->consumer->consumerVersion = $_POST['ext_lms'];$doSaveConsumer = true;}if (isset($_POST['tool_consumer_instance_guid'])) {if (is_null($this->consumer->consumerGuid)) {$this->consumer->consumerGuid = $_POST['tool_consumer_instance_guid'];$doSaveConsumer = true;} else if (!$this->consumer->protected) {$doSaveConsumer = ($this->consumer->consumerGuid !== $_POST['tool_consumer_instance_guid']);if ($doSaveConsumer) {$this->consumer->consumerGuid = $_POST['tool_consumer_instance_guid'];}}}if (isset($_POST['launch_presentation_css_url'])) {if ($this->consumer->cssPath !== $_POST['launch_presentation_css_url']) {$this->consumer->cssPath = $_POST['launch_presentation_css_url'];$doSaveConsumer = true;}} else if (isset($_POST['ext_launch_presentation_css_url']) &&($this->consumer->cssPath !== $_POST['ext_launch_presentation_css_url'])) {$this->consumer->cssPath = $_POST['ext_launch_presentation_css_url'];$doSaveConsumer = true;} else if (!empty($this->consumer->cssPath)) {$this->consumer->cssPath = null;$doSaveConsumer = true;}}// Persist changes to consumerif ($doSaveConsumer) {$this->consumer->save();}if ($this->ok && isset($this->context)) {$this->context->save();}if ($this->ok && isset($this->resourceLink)) {// Check if a share arrangement is in place for this resource link$this->ok = $this->checkForShare();// Persist changes to resource link$this->resourceLink->save();// Save the user instanceif (isset($_POST['lis_result_sourcedid'])) {if ($this->user->ltiResultSourcedId !== $_POST['lis_result_sourcedid']) {$this->user->ltiResultSourcedId = $_POST['lis_result_sourcedid'];$this->user->save();}} else if (!empty($this->user->ltiResultSourcedId)) {$this->user->ltiResultSourcedId = '';$this->user->save();}}return $this->ok;}/*** Check if a share arrangement is in place.** @return boolean True if no error is reported*/private function checkForShare(){$ok = true;$doSaveResourceLink = true;$id = $this->resourceLink->primaryResourceLinkId;$shareRequest = isset($_POST['custom_share_key']) && !empty($_POST['custom_share_key']);if ($shareRequest) {if (!$this->allowSharing) {$ok = false;$this->reason = 'Your sharing request has been refused because sharing is not being permitted.';} else {// Check if this is a new share key$shareKey = new ResourceLinkShareKey($this->resourceLink, $_POST['custom_share_key']);if (!is_null($shareKey->primaryConsumerKey) && !is_null($shareKey->primaryResourceLinkId)) {// Update resource link with sharing primary resource link details$key = $shareKey->primaryConsumerKey;$id = $shareKey->primaryResourceLinkId;$ok = ($key !== $this->consumer->getKey()) || ($id != $this->resourceLink->getId());if ($ok) {$this->resourceLink->primaryConsumerKey = $key;$this->resourceLink->primaryResourceLinkId = $id;$this->resourceLink->shareApproved = $shareKey->autoApprove;$ok = $this->resourceLink->save();if ($ok) {$doSaveResourceLink = false;$this->user->getResourceLink()->primaryConsumerKey = $key;$this->user->getResourceLink()->primaryResourceLinkId = $id;$this->user->getResourceLink()->shareApproved = $shareKey->autoApprove;$this->user->getResourceLink()->updated = time();// Remove share key$shareKey->delete();} else {$this->reason = 'An error occurred initialising your share arrangement.';}} else {$this->reason = 'It is not possible to share your resource link with yourself.';}}if ($ok) {$ok = !is_null($key);if (!$ok) {$this->reason = 'You have requested to share a resource link but none is available.';} else {$ok = (!is_null($this->user->getResourceLink()->shareApproved) && $this->user->getResourceLink()->shareApproved);if (!$ok) {$this->reason = 'Your share request is waiting to be approved.';}}}}} else {// Check no share is in place$ok = is_null($id);if (!$ok) {$this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.';}}// Look up primary resource linkif ($ok && !is_null($id)) {$consumer = new ToolConsumer($key, $this->dataConnector);$ok = !is_null($consumer->created);if ($ok) {$resourceLink = ResourceLink::fromConsumer($consumer, $id);$ok = !is_null($resourceLink->created);}if ($ok) {if ($doSaveResourceLink) {$this->resourceLink->save();}$this->resourceLink = $resourceLink;} else {$this->reason = 'Unable to load resource link being shared.';}}return $ok;}/*** Validate a parameter value from an array of permitted values.** @return boolean True if value is valid*/private function checkValue($value, $values, $reason){$ok = in_array($value, $values);if (!$ok && !empty($reason)) {$this->reason = sprintf($reason, $value);}return $ok;}}