<?php namespace IMSGlobal\LTI\ToolProvider; use DOMDocument; use DOMElement; use IMSGlobal\LTI\ToolProvider\DataConnector\DataConnector; use IMSGlobal\LTI\ToolProvider\Service; use IMSGlobal\LTI\HTTPMessage; use IMSGlobal\LTI\OAuth; /** * Class to represent a tool consumer resource link * * @author Stephen P Vickers <svickers@imsglobal.org> * @copyright IMS Global Learning Consortium Inc * @date 2016 * @version 3.0.2 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0 */> #[\AllowDynamicProperties]class ResourceLink { /** * Read action. */ const EXT_READ = 1; /** * Write (create/update) action. */ const EXT_WRITE = 2; /** * Delete action. */ const EXT_DELETE = 3; /** * Create action. */ const EXT_CREATE = 4; /** * Update action. */ const EXT_UPDATE = 5; /** * Decimal outcome type. */ const EXT_TYPE_DECIMAL = 'decimal'; /** * Percentage outcome type. */ const EXT_TYPE_PERCENTAGE = 'percentage'; /** * Ratio outcome type. */ const EXT_TYPE_RATIO = 'ratio'; /** * Letter (A-F) outcome type. */ const EXT_TYPE_LETTER_AF = 'letteraf'; /** * Letter (A-F) with optional +/- outcome type. */ const EXT_TYPE_LETTER_AF_PLUS = 'letterafplus'; /** * Pass/fail outcome type. */ const EXT_TYPE_PASS_FAIL = 'passfail'; /** * Free text outcome type. */ const EXT_TYPE_TEXT = 'freetext'; /** * Context title. * * @var string $title */ public $title = null; /** * Resource link ID as supplied in the last connection request. * * @var string $ltiResourceLinkId */ public $ltiResourceLinkId = null; /** * User group sets (null if the consumer does not support the groups enhancement) * * @var array $groupSets */ public $groupSets = null; /** * User groups (null if the consumer does not support the groups enhancement) * * @var array $groups */ public $groups = null; /** * Request for last service request. * * @var string $extRequest */ public $extRequest = null; /** * Request headers for last service request. * * @var array $extRequestHeaders */ public $extRequestHeaders = null; /** * Response from last service request. * * @var string $extResponse */ public $extResponse = null; /** * Response header from last service request. * * @var array $extResponseHeaders */ public $extResponseHeaders = null; /** * Consumer key value for resource link being shared (if any). * * @var string $primaryResourceLinkId */ public $primaryResourceLinkId = null; /** * Whether the sharing request has been approved by the primary resource link. * * @var boolean $shareApproved */ public $shareApproved = null; /** * Date/time when the object was created. * * @var int $created */ public $created = null; /** * Date/time when the object was last updated. * * @var int $updated */ public $updated = null; /** * Record ID for this resource link. * * @var int $id */ private $id = null; /** * Tool Consumer for this resource link. * * @var ToolConsumer $consumer */ private $consumer = null; /** * Tool Consumer ID for this resource link. * * @var int $consumerId */ private $consumerId = null; /** * Context for this resource link. * * @var Context $context */ private $context = null; /** * Context ID for this resource link. * * @var int $contextId */ private $contextId = null; /** * Setting values (LTI parameters, custom parameters and local parameters). * * @var array $settings */ private $settings = null; /** * Whether the settings value have changed since last saved. * * @var boolean $settingsChanged */ private $settingsChanged = false; /** * XML document for the last extension service request. * * @var string $extDoc */ private $extDoc = null; /** * XML node array for the last extension service request. * * @var array $extNodes */ private $extNodes = null; /** * Data connector object or string. * * @var mixed $dataConnector */ private $dataConnector = null; /** * Class constructor. */ public function __construct() { $this->initialize(); } /** * Initialise the resource link. */ public function initialize() { $this->title = ''; $this->settings = array(); $this->groupSets = null; $this->groups = null; $this->primaryResourceLinkId = null; $this->shareApproved = null; $this->created = null; $this->updated = null; } /** * Initialise the resource link. * * Pseudonym for initialize(). */ public function initialise() { $this->initialize(); } /** * Save the resource link to the database. * * @return boolean True if the resource link was successfully saved. */ public function save() { $ok = $this->getDataConnector()->saveResourceLink($this); if ($ok) { $this->settingsChanged = false; } return $ok; } /** * Delete the resource link from the database. * * @return boolean True if the resource link was successfully deleted. */ public function delete() { return $this->getDataConnector()->deleteResourceLink($this); } /** * Get tool consumer. * * @return ToolConsumer Tool consumer object for this resource link. */ public function getConsumer() { if (is_null($this->consumer)) { if (!is_null($this->context) || !is_null($this->contextId)) { $this->consumer = $this->getContext()->getConsumer(); } else { $this->consumer = ToolConsumer::fromRecordId($this->consumerId, $this->getDataConnector()); } } return $this->consumer; } /** * Set tool consumer ID. * * @param int $consumerId Tool Consumer ID for this resource link. */ public function setConsumerId($consumerId) { $this->consumer = null; $this->consumerId = $consumerId; } /** * Get context. * * @return object LTIContext object for this resource link. */ public function getContext() { if (is_null($this->context) && !is_null($this->contextId)) { $this->context = Context::fromRecordId($this->contextId, $this->getDataConnector()); } return $this->context; } /** * Get context record ID. * * @return int Context record ID for this resource link. */ public function getContextId() { return $this->contextId; } /** * Set context ID. * * @param int $contextId Context ID for this resource link. */ public function setContextId($contextId) { $this->context = null; $this->contextId = $contextId; } /** * Get tool consumer key. * * @return string Consumer key value for this resource link. */ public function getKey() { return $this->getConsumer()->getKey(); } /** * Get resource link ID. * * @return string ID for this resource link. */ public function getId() { return $this->ltiResourceLinkId; } /** * Get resource link record ID. * * @return int Record ID for this resource link. */ public function getRecordId() { return $this->id; } /** * Set resource link record ID. * * @param int $id Record ID for this resource link. */ public function setRecordId($id) { $this->id = $id; } /** * Get the data connector. * * @return mixed Data connector object or string */ public function getDataConnector() { return $this->dataConnector; } /** * Get a setting value. * * @param string $name Name of setting * @param string $default Value to return if the setting does not exist (optional, default is an empty string) * * @return string Setting value */ public function getSetting($name, $default = '') { if (array_key_exists($name, $this->settings)) { $value = $this->settings[$name]; } else { $value = $default; } return $value; } /** * Set a setting value. * * @param string $name Name of setting * @param string $value Value to set, use an empty value to delete a setting (optional, default is null) */ public function setSetting($name, $value = null) { $old_value = $this->getSetting($name); if ($value !== $old_value) { if (!empty($value)) { $this->settings[$name] = $value; } else { unset($this->settings[$name]); } $this->settingsChanged = true; } } /** * Get an array of all setting values. * * @return array Associative array of setting values */ public function getSettings() { return $this->settings; } /** * Set an array of all setting values. * * @param array $settings Associative array of setting values */ public function setSettings($settings) { $this->settings = $settings; } /** * Save setting values. * * @return boolean True if the settings were successfully saved */ public function saveSettings() { if ($this->settingsChanged) { $ok = $this->save(); } else { $ok = true; } return $ok; } /** * Check if the Outcomes service is supported. * * @return boolean True if this resource link supports the Outcomes service (either the LTI 1.1 or extension service) */ public function hasOutcomesService() { $url = $this->getSetting('ext_ims_lis_basic_outcome_url') . $this->getSetting('lis_outcome_service_url'); return !empty($url); } /** * Check if the Memberships extension service is supported. * * @return boolean True if this resource link supports the Memberships extension service */ public function hasMembershipsService() { $url = $this->getSetting('ext_ims_lis_memberships_url'); return !empty($url); } /** * Check if the Setting extension service is supported. * * @return boolean True if this resource link supports the Setting extension service */ public function hasSettingService() { $url = $this->getSetting('ext_ims_lti_tool_setting_url'); return !empty($url); } /** * Perform an Outcomes service request. * * @param int $action The action type constant * @param Outcome $ltiOutcome Outcome object * @param User $user User object * * @return boolean True if the request was successfully processed */ public function doOutcomesService($action, $ltiOutcome, $user) { $response = false; $this->extResponse = null; // Lookup service details from the source resource link appropriate to the user (in case the destination is being shared) $sourceResourceLink = $user->getResourceLink(); $sourcedId = $user->ltiResultSourcedId; // Use LTI 1.1 service in preference to extension service if it is available $urlLTI11 = $sourceResourceLink->getSetting('lis_outcome_service_url'); $urlExt = $sourceResourceLink->getSetting('ext_ims_lis_basic_outcome_url'); if ($urlExt || $urlLTI11) { switch ($action) { case self::EXT_READ: if ($urlLTI11 && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL)) { $do = 'readResult'; } else if ($urlExt) { $urlLTI11 = null; $do = 'basic-lis-readresult'; } break; case self::EXT_WRITE: if ($urlLTI11 && $this->checkValueType($ltiOutcome, array(self::EXT_TYPE_DECIMAL))) { $do = 'replaceResult'; } else if ($this->checkValueType($ltiOutcome)) { $urlLTI11 = null; $do = 'basic-lis-updateresult'; } break; case self::EXT_DELETE: if ($urlLTI11 && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL)) { $do = 'deleteResult'; } else if ($urlExt) { $urlLTI11 = null; $do = 'basic-lis-deleteresult'; } break; } } if (isset($do)) { $value = $ltiOutcome->getValue(); if (is_null($value)) { $value = ''; } if ($urlLTI11) { $xml = ''; if ($action === self::EXT_WRITE) { $xml = <<<EOF <result> <resultScore> <language>{$ltiOutcome->language}</language> <textString>{$value}</textString> </resultScore> </result> EOF; } $sourcedId = htmlentities($sourcedId); $xml = <<<EOF <resultRecord> <sourcedGUID> <sourcedId>{$sourcedId}</sourcedId> </sourcedGUID>{$xml} </resultRecord> EOF; if ($this->doLTI11Service($do, $urlLTI11, $xml)) { switch ($action) { case self::EXT_READ: if (!isset($this->extNodes['imsx_POXBody']["{$do}Response"]['result']['resultScore']['textString'])) { break; } else { $ltiOutcome->setValue($this->extNodes['imsx_POXBody']["{$do}Response"]['result']['resultScore']['textString']); } case self::EXT_WRITE: case self::EXT_DELETE: $response = true; break; } } } else { $params = array(); $params['sourcedid'] = $sourcedId; $params['result_resultscore_textstring'] = $value; if (!empty($ltiOutcome->language)) { $params['result_resultscore_language'] = $ltiOutcome->language; } if (!empty($ltiOutcome->status)) { $params['result_statusofresult'] = $ltiOutcome->status; } if (!empty($ltiOutcome->date)) { $params['result_date'] = $ltiOutcome->date; } if (!empty($ltiOutcome->type)) { $params['result_resultvaluesourcedid'] = $ltiOutcome->type; } if (!empty($ltiOutcome->data_source)) { $params['result_datasource'] = $ltiOutcome->data_source; } if ($this->doService($do, $urlExt, $params)) { switch ($action) { case self::EXT_READ: if (isset($this->extNodes['result']['resultscore']['textstring'])) { $response = $this->extNodes['result']['resultscore']['textstring']; } break; case self::EXT_WRITE: case self::EXT_DELETE: $response = true; break; } } } if (is_array($response) && (count($response) <= 0)) { $response = ''; } } return $response; } /** * 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 = array(); $oldUsers = $this->getUserResultSourcedIDs(true, ToolProvider::ID_SCOPE_RESOURCE); $this->extResponse = null; $url = $this->getSetting('ext_ims_lis_memberships_url'); $params = array(); $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 = array(); $this->groups = array(); } else { $ok = $this->doService('basic-lis-readmembershipsforcontext', $url, $params); } if ($ok) { if (!isset($this->extNodes['memberships']['member'])) { $members = array(); } else if (!isset($this->extNodes['memberships']['member'][0])) { $members = array(); $members[0] = $this->extNodes['memberships']['member']; } else { $members = $this->extNodes['memberships']['member']; } for ($i = 0; $i < count($members); $i++) { $user = User::fromResourceLink($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->getConsumer()->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 = array(); } else if (!isset($members[$i]['groups']['group'][0])) { $groups = array(); $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] = array('title' => $group['set']['title'], 'groups' => array(), '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']] = array('title' => $group['title'], 'set' => $set_id); } else { $this->groups[$group['id']] = array('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($oldUsers[$user->getId(ToolProvider::ID_SCOPE_RESOURCE)]); } // Delete any old users which were not in the latest list from the tool consumer foreach ($oldUsers as $id => $user) { $user->delete(); } } else { $users = false; } return $users; } /** * Perform a Setting service request. * * @param int $action The action type constant * @param string $value The setting value (optional, default is null) * * @return mixed The setting value for a read action, true if a write or delete action was successful, otherwise false */ public function doSettingService($action, $value = null) { $response = false; $this->extResponse = null; switch ($action) { case self::EXT_READ: $do = 'basic-lti-loadsetting'; break; case self::EXT_WRITE: $do = 'basic-lti-savesetting'; break; case self::EXT_DELETE: $do = 'basic-lti-deletesetting'; break; } if (isset($do)) { $url = $this->getSetting('ext_ims_lti_tool_setting_url'); $params = array(); $params['id'] = $this->getSetting('ext_ims_lti_tool_setting_id'); if (is_null($value)) { $value = ''; } $params['setting'] = $value; if ($this->doService($do, $url, $params)) { switch ($action) { case self::EXT_READ: if (isset($this->extNodes['setting']['value'])) { $response = $this->extNodes['setting']['value']; if (is_array($response)) { $response = ''; } } break; case self::EXT_WRITE: $this->setSetting('ext_ims_lti_tool_setting', $value); $this->saveSettings(); $response = true; break; case self::EXT_DELETE: $response = true; break; } } } return $response; } /** * Check if the Tool Settings service is supported. * * @return boolean True if this resource link supports the Tool Settings service */ public function hasToolSettingsService() { $url = $this->getSetting('custom_link_setting_url'); return !empty($url); } /** * Get Tool Settings. * * @param int $mode Mode for request (optional, default is current level only) * @param boolean $simple True if all the simple media type is to be used (optional, default is true) * * @return mixed The array of settings if successful, otherwise false */ public function getToolSettings($mode = Service\ToolSettings::MODE_CURRENT_LEVEL, $simple = true) { $url = $this->getSetting('custom_link_setting_url'); $service = new Service\ToolSettings($this, $url, $simple); $response = $service->get($mode); return $response; } /** * Perform a Tool Settings service request. * * @param array $settings An associative array of settings (optional, default is none) * * @return boolean True if action was successful, otherwise false */ public function setToolSettings($settings = array()) { $url = $this->getSetting('custom_link_setting_url'); $service = new Service\ToolSettings($this, $url); $response = $service->set($settings); return $response; } /** * Check if the Membership service is supported. * * @return boolean True if this resource link supports the Membership service */ public function hasMembershipService() { $has = !empty($this->contextId); if ($has) { $has = !empty($this->getContext()->getSetting('custom_context_memberships_url')); } return $has; } /** * Get Memberships. * * @return mixed The array of User objects if successful, otherwise false */ public function getMembership() { $response = false; if (!empty($this->contextId)) { $url = $this->getContext()->getSetting('custom_context_memberships_url'); if (!empty($url)) { $service = new Service\Membership($this, $url); $response = $service->get(); } } return $response; } /** * Obtain an array of User objects for users with a result sourcedId. * * The array may include users from other resource links which are sharing this resource link. * It may also be optionally indexed by the user ID of a specified scope. * * @param boolean $localOnly True if only users from this resource link are to be returned, not users from shared resource links (optional, default is false) * @param int $idScope Scope to use for ID values (optional, default is null for consumer default) * * @return array Array of User objects */ public function getUserResultSourcedIDs($localOnly = false, $idScope = null) { return $this->getDataConnector()->getUserResultSourcedIDsResourceLink($this, $localOnly, $idScope); } /** * Get an array of ResourceLinkShare objects for each resource link which is sharing this context. * * @return array Array of ResourceLinkShare objects */ public function getShares() { return $this->getDataConnector()->getSharesResourceLink($this); } /** * Class constructor from consumer. * * @param ToolConsumer $consumer Consumer object * @param string $ltiResourceLinkId Resource link ID value * @param string $tempId Temporary Resource link ID value (optional, default is null) * @return ResourceLink */ public static function fromConsumer($consumer, $ltiResourceLinkId, $tempId = null) { $resourceLink = new ResourceLink(); $resourceLink->consumer = $consumer; $resourceLink->dataConnector = $consumer->getDataConnector(); $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; if (!empty($ltiResourceLinkId)) { $resourceLink->load(); if (is_null($resourceLink->id) && !empty($tempId)) { $resourceLink->ltiResourceLinkId = $tempId; $resourceLink->load(); $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; } } return $resourceLink; } /** * Class constructor from context. * * @param Context $context Context object * @param string $ltiResourceLinkId Resource link ID value * @param string $tempId Temporary Resource link ID value (optional, default is null) * @return ResourceLink */ public static function fromContext($context, $ltiResourceLinkId, $tempId = null) { $resourceLink = new ResourceLink(); $resourceLink->setContextId($context->getRecordId()); $resourceLink->context = $context; $resourceLink->dataConnector = $context->getDataConnector(); $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; if (!empty($ltiResourceLinkId)) { $resourceLink->load(); if (is_null($resourceLink->id) && !empty($tempId)) { $resourceLink->ltiResourceLinkId = $tempId; $resourceLink->load(); $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; } } return $resourceLink; } /** * Load the resource link from the database. * * @param int $id Record ID of resource link * @param DataConnector $dataConnector Database connection object * * @return ResourceLink ResourceLink object */ public static function fromRecordId($id, $dataConnector) { $resourceLink = new ResourceLink(); $resourceLink->dataConnector = $dataConnector; $resourceLink->load($id); return $resourceLink; } ### ### PRIVATE METHODS ### /** * Load the resource link from the database. * * @param int $id Record ID of resource link (optional, default is null) * * @return boolean True if resource link was successfully loaded */ private function load($id = null) { $this->initialize(); $this->id = $id; return $this->getDataConnector()->loadResourceLink($this); } /** * Convert data type of value to a supported type if possible. * * @param Outcome $ltiOutcome Outcome object * @param string[] $supportedTypes Array of outcome types to be supported (optional, default is null to use supported types reported in the last launch for this resource link) * * @return boolean True if the type/value are valid and supported */ private function checkValueType($ltiOutcome, $supportedTypes = null) { if (empty($supportedTypes)) { $supportedTypes = explode(',', str_replace(' ', '', strtolower($this->getSetting('ext_ims_lis_resultvalue_sourcedids', self::EXT_TYPE_DECIMAL)))); } $type = $ltiOutcome->type; $value = $ltiOutcome->getValue(); // Check whether the type is supported or there is no value $ok = in_array($type, $supportedTypes) || (strlen($value) <= 0); if (!$ok) { // Convert numeric values to decimal if ($type === self::EXT_TYPE_PERCENTAGE) { if (substr($value, -1) === '%') { $value = substr($value, 0, -1); } $ok = is_numeric($value) && ($value >= 0) && ($value <= 100); if ($ok) { $ltiOutcome->setValue($value / 100); $ltiOutcome->type = self::EXT_TYPE_DECIMAL; } } else if ($type === self::EXT_TYPE_RATIO) { $parts = explode('/', $value, 2); $ok = (count($parts) === 2) && is_numeric($parts[0]) && is_numeric($parts[1]) && ($parts[0] >= 0) && ($parts[1] > 0); if ($ok) { $ltiOutcome->setValue($parts[0] / $parts[1]); $ltiOutcome->type = self::EXT_TYPE_DECIMAL; } // Convert letter_af to letter_af_plus or text } else if ($type === self::EXT_TYPE_LETTER_AF) { if (in_array(self::EXT_TYPE_LETTER_AF_PLUS, $supportedTypes)) { $ok = true; $ltiOutcome->type = self::EXT_TYPE_LETTER_AF_PLUS; } else if (in_array(self::EXT_TYPE_TEXT, $supportedTypes)) { $ok = true; $ltiOutcome->type = self::EXT_TYPE_TEXT; } // Convert letter_af_plus to letter_af or text } else if ($type === self::EXT_TYPE_LETTER_AF_PLUS) { if (in_array(self::EXT_TYPE_LETTER_AF, $supportedTypes) && (strlen($value) === 1)) { $ok = true; $ltiOutcome->type = self::EXT_TYPE_LETTER_AF; } else if (in_array(self::EXT_TYPE_TEXT, $supportedTypes)) { $ok = true; $ltiOutcome->type = self::EXT_TYPE_TEXT; } // Convert text to decimal } else if ($type === self::EXT_TYPE_TEXT) { $ok = is_numeric($value) && ($value >= 0) && ($value <=1); if ($ok) { $ltiOutcome->type = self::EXT_TYPE_DECIMAL; } else if (substr($value, -1) === '%') { $value = substr($value, 0, -1); $ok = is_numeric($value) && ($value >= 0) && ($value <=100); if ($ok) { if (in_array(self::EXT_TYPE_PERCENTAGE, $supportedTypes)) { $ltiOutcome->type = self::EXT_TYPE_PERCENTAGE; } else { $ltiOutcome->setValue($value / 100); $ltiOutcome->type = self::EXT_TYPE_DECIMAL; } } } } } return $ok; } /** * Send a service request to the tool consumer. * * @param string $type Message type value * @param string $url URL to send request to * @param array $params Associative array of parameter values to be passed * * @return boolean True if the request successfully obtained a response */ private function doService($type, $url, $params) { $ok = false; $this->extRequest = null; $this->extRequestHeaders = ''; $this->extResponse = null; $this->extResponseHeaders = ''; if (!empty($url)) { $params = $this->getConsumer()->signParameters($url, $type, $this->getConsumer()->ltiVersion, $params); // Connect to tool consumer $http = new HTTPMessage($url, 'POST', $params); // Parse XML response if ($http->send()) { $this->extResponse = $http->response; $this->extResponseHeaders = $http->responseHeaders; try { $this->extDoc = new DOMDocument(); $this->extDoc->loadXML($http->response); $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement); if (isset($this->extNodes['statusinfo']['codemajor']) && ($this->extNodes['statusinfo']['codemajor'] === 'Success')) { $ok = true; } } catch (\Exception $e) { } } $this->extRequest = $http->request; $this->extRequestHeaders = $http->requestHeaders; } return $ok; } /** * Send a service request to the tool consumer. * * @param string $type Message type value * @param string $url URL to send request to * @param string $xml XML of message request * * @return boolean True if the request successfully obtained a response */ private function doLTI11Service($type, $url, $xml) { $ok = false; $this->extRequest = null; $this->extRequestHeaders = ''; $this->extResponse = null; $this->extResponseHeaders = ''; if (!empty($url)) { $id = uniqid(); $xmlRequest = <<< EOD <?xml version = "1.0" encoding = "UTF-8"?> <imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0"> <imsx_POXHeader> <imsx_POXRequestHeaderInfo> <imsx_version>V1.0</imsx_version> <imsx_messageIdentifier>{$id}</imsx_messageIdentifier> </imsx_POXRequestHeaderInfo> </imsx_POXHeader> <imsx_POXBody> <{$type}Request> {$xml} </{$type}Request> </imsx_POXBody> </imsx_POXEnvelopeRequest> EOD; // Calculate body hash $hash = base64_encode(sha1($xmlRequest, true)); $params = array('oauth_body_hash' => $hash); // Add OAuth signature $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA1(); $consumer = new OAuth\OAuthConsumer($this->getConsumer()->getKey(), $this->getConsumer()->secret, null); $req = OAuth\OAuthRequest::from_consumer_and_token($consumer, null, 'POST', $url, $params); $req->sign_request($hmacMethod, $consumer, null); $params = $req->get_parameters(); $header = $req->to_header(); $header .= "\nContent-Type: application/xml"; // Connect to tool consumer $http = new HTTPMessage($url, 'POST', $xmlRequest, $header); // Parse XML response if ($http->send()) { $this->extResponse = $http->response; $this->extResponseHeaders = $http->responseHeaders; try { $this->extDoc = new DOMDocument(); $this->extDoc->loadXML($http->response); $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement); if (isset($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor']) && ($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor'] === 'success')) { $ok = true; } } catch (\Exception $e) { } } $this->extRequest = $http->request; $this->extRequestHeaders = $http->requestHeaders; } return $ok; } /** * Convert DOM nodes to array. * * @param DOMElement $node XML element * * @return array Array of XML document elements */ private function domnodeToArray($node) { $output = ''; switch ($node->nodeType) { case XML_CDATA_SECTION_NODE: case XML_TEXT_NODE: $output = trim($node->textContent); break; case XML_ELEMENT_NODE: for ($i = 0; $i < $node->childNodes->length; $i++) { $child = $node->childNodes->item($i); $v = $this->domnodeToArray($child); if (isset($child->tagName)) { $t = $child->tagName; if (!isset($output[$t])) { $output[$t] = array(); } $output[$t][] = $v; } else { $s = (string) $v; if (strlen($s) > 0) { $output = $s; } } } if (is_array($output)) { if ($node->attributes->length) { $a = array(); foreach ($node->attributes as $attrName => $attrNode) { $a[$attrName] = (string) $attrNode->value; } $output['@attributes'] = $a; } foreach ($output as $t => $v) { if (is_array($v) && count($v)==1 && $t!='@attributes') { $output[$t] = $v[0]; } } } break; } return $output; } }