Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file contains a class definition for the Tool Proxy resource
  19   *
  20   * @package    ltiservice_toolproxy
  21   * @copyright  2014 Vital Source Technologies http://vitalsource.com
  22   * @author     Stephen Vickers
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  namespace ltiservice_toolproxy\local\resources;
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  require_once($CFG->dirroot . '/mod/lti/OAuth.php');
  32  require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
  33  
  34  // TODO: Switch to core oauthlib once implemented - MDL-30149.
  35  use moodle\mod\lti as lti;
  36  
  37  /**
  38   * A resource implementing the Tool Proxy.
  39   *
  40   * @package    ltiservice_toolproxy
  41   * @since      Moodle 2.8
  42   * @copyright  2014 Vital Source Technologies http://vitalsource.com
  43   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class toolproxy extends \mod_lti\local\ltiservice\resource_base {
  46  
  47      /**
  48       * Class constructor.
  49       *
  50       * @param ltiservice_toolproxy\local\resources\toolproxy $service Service instance
  51       */
  52      public function __construct($service) {
  53  
  54          parent::__construct($service);
  55          $this->id = 'ToolProxy.collection';
  56          $this->template = '/toolproxy';
  57          $this->formats[] = 'application/vnd.ims.lti.v2.toolproxy+json';
  58          $this->methods[] = 'POST';
  59  
  60      }
  61  
  62      /**
  63       * Execute the request for this resource.
  64       *
  65       * @param mod_lti\local\ltiservice\response $response  Response object for this request.
  66       */
  67      public function execute($response) {
  68  
  69          $ok = $this->check_tool(null, $response->get_request_data());
  70          $ok = $ok && ($this->get_service()->get_tool_proxy());
  71          if ($ok) {
  72              $toolproxy = $this->get_service()->get_tool_proxy();
  73          }
  74          if (!$ok) {
  75              $toolproxy = null;
  76              $response->set_code(401);
  77          }
  78          $tools = array();
  79  
  80          // Ensure all required elements are present in the Tool Proxy.
  81          if ($ok) {
  82              $toolproxyjson = json_decode($response->get_request_data());
  83              $ok = !empty($toolproxyjson);
  84              if (!$ok) {
  85                  debugging('Tool proxy is not properly formed JSON');
  86              } else {
  87                  $ok = isset($toolproxyjson->tool_profile->product_instance->product_info->product_family->vendor->code);
  88                  $ok = $ok && isset($toolproxyjson->security_contract->shared_secret);
  89                  $ok = $ok && isset($toolproxyjson->tool_profile->resource_handler);
  90                  if (!$ok) {
  91                      debugging('One or more missing elements from tool proxy: vendor code, shared secret or resource handlers');
  92                  }
  93              }
  94          }
  95  
  96          // Check all capabilities requested were offered.
  97          if ($ok) {
  98              $offeredcapabilities = explode("\n", $toolproxy->capabilityoffered);
  99              $resources = $toolproxyjson->tool_profile->resource_handler;
 100              $errors = array();
 101              foreach ($resources as $resource) {
 102                  if (isset($resource->message)) {
 103                      foreach ($resource->message as $message) {
 104                          if (!in_array($message->message_type, $offeredcapabilities)) {
 105                              $errors[] = $message->message_type;
 106                          } else if (isset($resource->parameter)) {
 107                              foreach ($message->parameter as $parameter) {
 108                                  if (isset($parameter->variable) && !in_array($parameter->variable, $offeredcapabilities)) {
 109                                      $errors[] = $parameter->variable;
 110                                  }
 111                              }
 112                          }
 113                      }
 114                  }
 115              }
 116              if (count($errors) > 0) {
 117                  $ok = false;
 118                  debugging('Tool proxy contains capabilities which were not offered: ' . implode(', ', $errors));
 119              }
 120          }
 121  
 122          // Check all services requested were offered (only tool services currently supported).
 123          $requestsbasicoutcomes = false;
 124          if ($ok && isset($toolproxyjson->security_contract->tool_service)) {
 125              $contexts = lti_get_contexts($toolproxyjson);
 126              $profileservice = lti_get_service_by_name('profile');
 127              $profileservice->set_tool_proxy($toolproxy);
 128              $context = $profileservice->get_service_path() . $profileservice->get_resources()[0]->get_path() . '#';
 129              $offeredservices = explode("\n", $toolproxy->serviceoffered);
 130              $services = lti_get_services();
 131              $tpservices = $toolproxyjson->security_contract->tool_service;
 132              $errors = array();
 133              foreach ($tpservices as $service) {
 134                  $fqid = lti_get_fqid($contexts, $service->service);
 135                  $requestsbasicoutcomes = $requestsbasicoutcomes || (substr($fqid, -13) === 'Outcomes.LTI1');
 136                  if (substr($fqid, 0, strlen($context)) !== $context) {
 137                      $errors[] = $service->service;
 138                  } else {
 139                      $id = explode('#', $fqid, 2);
 140                      $aservice = lti_get_service_by_resource_id($services, $id[1]);
 141                      $classname = explode('\\', get_class($aservice));
 142                      if (empty($aservice) || !in_array($classname[count($classname) - 1], $offeredservices)) {
 143                          $errors[] = $service->service;
 144                      }
 145                  }
 146              }
 147              if (count($errors) > 0) {
 148                  $ok = false;
 149                  debugging('Tool proxy contains services which were not offered: ' . implode(', ', $errors));
 150              }
 151          }
 152  
 153          // Extract all launchable tools from the resource handlers.
 154          if ($ok) {
 155              $resources = $toolproxyjson->tool_profile->resource_handler;
 156              $messagetypes = [
 157                  'basic-lti-launch-request',
 158                  'ContentItemSelectionRequest',
 159              ];
 160              foreach ($resources as $resource) {
 161                  $launchable = false;
 162                  $messages = array();
 163                  $tool = new \stdClass();
 164  
 165                  $iconinfo = null;
 166                  if (is_array($resource->icon_info)) {
 167                      $iconinfo = $resource->icon_info[0];
 168                  } else {
 169                      $iconinfo = $resource->icon_info;
 170                  }
 171                  if (isset($iconinfo) && isset($iconinfo->default_location) && isset($iconinfo->default_location->path)) {
 172                      $tool->iconpath = $iconinfo->default_location->path;
 173                  }
 174  
 175                  foreach ($resource->message as $message) {
 176                      if (in_array($message->message_type, $messagetypes)) {
 177                          $launchable = $launchable || ($message->message_type === 'basic-lti-launch-request');
 178                          $messages[$message->message_type] = $message;
 179                      }
 180                  }
 181                  if (!$launchable) {
 182                      continue;
 183                  }
 184                  $tool->name = $resource->resource_name->default_value;
 185                  $tool->messages = $messages;
 186                  $tools[] = $tool;
 187              }
 188              $ok = count($tools) > 0;
 189              if (!$ok) {
 190                  debugging('No launchable messages found in tool proxy');
 191              }
 192          }
 193  
 194          // Add tools and custom parameters.
 195          if ($ok) {
 196              $baseurl = '';
 197              if (isset($toolproxyjson->tool_profile->base_url_choice[0]->default_base_url)) {
 198                  $baseurl = $toolproxyjson->tool_profile->base_url_choice[0]->default_base_url;
 199              }
 200              $securebaseurl = '';
 201              if (isset($toolproxyjson->tool_profile->base_url_choice[0]->secure_base_url)) {
 202                  $securebaseurl = $toolproxyjson->tool_profile->base_url_choice[0]->secure_base_url;
 203              }
 204              foreach ($tools as $tool) {
 205                  $messages = $tool->messages;
 206                  $launchrequest = $messages['basic-lti-launch-request'];
 207                  $config = new \stdClass();
 208                  $config->lti_toolurl = "{$baseurl}{$launchrequest->path}";
 209                  $config->lti_typename = $tool->name;
 210                  $config->lti_coursevisible = 1;
 211                  $config->lti_forcessl = 0;
 212                  if (isset($messages['ContentItemSelectionRequest'])) {
 213                      $contentitemrequest = $messages['ContentItemSelectionRequest'];
 214                      $config->lti_contentitem = 1;
 215                      if ($launchrequest->path !== $contentitemrequest->path) {
 216                          $config->lti_toolurl_ContentItemSelectionRequest = $baseurl . $contentitemrequest->path;
 217                      }
 218                      $contentitemcapabilities = implode("\n", $contentitemrequest->enabled_capability);
 219                      $config->lti_enabledcapability_ContentItemSelectionRequest = $contentitemcapabilities;
 220                      $contentitemparams = self::lti_extract_parameters($contentitemrequest->parameter);
 221                      $config->lti_parameter_ContentItemSelectionRequest = $contentitemparams;
 222                  }
 223  
 224                  $type = new \stdClass();
 225                  $type->state = LTI_TOOL_STATE_PENDING;
 226                  $type->ltiversion = LTI_VERSION_2;
 227                  $type->toolproxyid = $toolproxy->id;
 228                  // Ensure gradebook column is created.
 229                  if ($requestsbasicoutcomes && !in_array('BasicOutcome.url', $launchrequest->enabled_capability)) {
 230                      $launchrequest->enabled_capability[] = 'BasicOutcome.url';
 231                  }
 232                  if ($requestsbasicoutcomes && !in_array('BasicOutcome.sourcedId', $launchrequest->enabled_capability)) {
 233                      $launchrequest->enabled_capability[] = 'BasicOutcome.sourcedId';
 234                  }
 235                  $type->enabledcapability = implode("\n", $launchrequest->enabled_capability);
 236                  $type->parameter = self::lti_extract_parameters($launchrequest->parameter);
 237  
 238                  if (!empty($tool->iconpath)) {
 239                      $type->icon = "{$baseurl}{$tool->iconpath}";
 240                      if (!empty($securebaseurl)) {
 241                          $type->secureicon = "{$securebaseurl}{$tool->iconpath}";
 242                      }
 243                  }
 244  
 245                  $ok = $ok && (lti_add_type($type, $config) !== false);
 246              }
 247              if (isset($toolproxyjson->custom)) {
 248                  lti_set_tool_settings($toolproxyjson->custom, $toolproxy->id);
 249              }
 250          }
 251  
 252          if (!empty($toolproxy)) {
 253              if ($ok) {
 254                  // If all went OK accept the tool proxy.
 255                  $toolproxy->state = LTI_TOOL_PROXY_STATE_ACCEPTED;
 256                  $toolproxy->toolproxy = $response->get_request_data();
 257                  $toolproxy->secret = $toolproxyjson->security_contract->shared_secret;
 258                  $toolproxy->vendorcode = $toolproxyjson->tool_profile->product_instance->product_info->product_family->vendor->code;
 259  
 260                  $url = $this->get_endpoint();
 261                  $body = <<< EOD
 262  {
 263    "@context" : "http://purl.imsglobal.org/ctx/lti/v2/ToolProxyId",
 264    "@type" : "ToolProxy",
 265    "@id" : "{$url}",
 266    "tool_proxy_guid" : "{$toolproxy->guid}"
 267  }
 268  EOD;
 269                  $response->set_code(201);
 270                  $response->set_content_type('application/vnd.ims.lti.v2.toolproxy.id+json');
 271                  $response->set_body($body);
 272              } else {
 273                  // Otherwise reject the tool proxy.
 274                  $toolproxy->state = LTI_TOOL_PROXY_STATE_REJECTED;
 275                  $response->set_code(400);
 276              }
 277              lti_update_tool_proxy($toolproxy);
 278          } else {
 279              $response->set_code(400);
 280          }
 281      }
 282  
 283      /**
 284       * Extracts the message parameters from the tool proxy entry
 285       *
 286       * @param array $parameters     Parameter section of a message
 287       *
 288       * @return String  containing parameters
 289       */
 290      private static function lti_extract_parameters($parameters) {
 291  
 292          $params = array();
 293          foreach ($parameters as $parameter) {
 294              if (isset($parameter->variable)) {
 295                  $value = '$' . $parameter->variable;
 296              } else {
 297                  $value = $parameter->fixed;
 298                  if (strlen($value) > 0) {
 299                      $first = substr($value, 0, 1);
 300                      if (($first == '$') || ($first == '\\')) {
 301                          $value = '\\' . $value;
 302                      }
 303                  }
 304              }
 305              $params[] = "{$parameter->name}={$value}";
 306          }
 307  
 308          return implode("\n", $params);
 309  
 310      }
 311  
 312  }