<?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;

    }

}
