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.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   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  // This file is part of BasicLTI4Moodle
  18  //
  19  // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
  20  // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
  21  // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
  22  // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
  23  // are already supporting or going to support BasicLTI. This project Implements the consumer
  24  // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
  25  // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
  26  // at the GESSI research group at UPC.
  27  // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
  28  // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
  29  // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
  30  //
  31  // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
  32  // of the Universitat Politecnica de Catalunya http://www.upc.edu
  33  // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu.
  34  
  35  /**
  36   * This file contains the library of functions and constants for the lti module
  37   *
  38   * @package mod_lti
  39   * @copyright  2009 Marc Alier, Jordi Piguillem, Nikolas Galanis
  40   *  marc.alier@upc.edu
  41   * @copyright  2009 Universitat Politecnica de Catalunya http://www.upc.edu
  42   * @author     Marc Alier
  43   * @author     Jordi Piguillem
  44   * @author     Nikolas Galanis
  45   * @author     Chris Scribner
  46   * @copyright  2015 Vital Source Technologies http://vitalsource.com
  47   * @author     Stephen Vickers
  48   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  49   */
  50  
  51  defined('MOODLE_INTERNAL') || die;
  52  
  53  // TODO: Switch to core oauthlib once implemented - MDL-30149.
  54  use mod_lti\helper;
  55  use moodle\mod\lti as lti;
  56  use Firebase\JWT\JWT;
  57  use Firebase\JWT\JWK;
  58  use Firebase\JWT\Key;
  59  use mod_lti\local\ltiopenid\jwks_helper;
  60  use mod_lti\local\ltiopenid\registration_helper;
  61  
  62  global $CFG;
  63  require_once($CFG->dirroot.'/mod/lti/OAuth.php');
  64  require_once($CFG->libdir.'/weblib.php');
  65  require_once($CFG->dirroot . '/course/modlib.php');
  66  require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
  67  
  68  define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i');
  69  
  70  define('LTI_LAUNCH_CONTAINER_DEFAULT', 1);
  71  define('LTI_LAUNCH_CONTAINER_EMBED', 2);
  72  define('LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS', 3);
  73  define('LTI_LAUNCH_CONTAINER_WINDOW', 4);
  74  define('LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW', 5);
  75  
  76  define('LTI_TOOL_STATE_ANY', 0);
  77  define('LTI_TOOL_STATE_CONFIGURED', 1);
  78  define('LTI_TOOL_STATE_PENDING', 2);
  79  define('LTI_TOOL_STATE_REJECTED', 3);
  80  define('LTI_TOOL_PROXY_TAB', 4);
  81  
  82  define('LTI_TOOL_PROXY_STATE_CONFIGURED', 1);
  83  define('LTI_TOOL_PROXY_STATE_PENDING', 2);
  84  define('LTI_TOOL_PROXY_STATE_ACCEPTED', 3);
  85  define('LTI_TOOL_PROXY_STATE_REJECTED', 4);
  86  
  87  define('LTI_SETTING_NEVER', 0);
  88  define('LTI_SETTING_ALWAYS', 1);
  89  define('LTI_SETTING_DELEGATE', 2);
  90  
  91  define('LTI_COURSEVISIBLE_NO', 0);
  92  define('LTI_COURSEVISIBLE_PRECONFIGURED', 1);
  93  define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2);
  94  
  95  define('LTI_VERSION_1', 'LTI-1p0');
  96  define('LTI_VERSION_2', 'LTI-2p0');
  97  define('LTI_VERSION_1P3', '1.3.0');
  98  define('LTI_RSA_KEY', 'RSA_KEY');
  99  define('LTI_JWK_KEYSET', 'JWK_KEYSET');
 100  
 101  define('LTI_DEFAULT_ORGID_SITEID', 'SITEID');
 102  define('LTI_DEFAULT_ORGID_SITEHOST', 'SITEHOST');
 103  
 104  define('LTI_ACCESS_TOKEN_LIFE', 3600);
 105  
 106  // Standard prefix for JWT claims.
 107  define('LTI_JWT_CLAIM_PREFIX', 'https://purl.imsglobal.org/spec/lti');
 108  
 109  /**
 110   * Return the mapping for standard message types to JWT message_type claim.
 111   *
 112   * @return array
 113   */
 114  function lti_get_jwt_message_type_mapping() {
 115      return array(
 116          'basic-lti-launch-request' => 'LtiResourceLinkRequest',
 117          'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest',
 118          'LtiDeepLinkingResponse' => 'ContentItemSelection',
 119          'LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest',
 120      );
 121  }
 122  
 123  /**
 124   * Return the mapping for standard message parameters to JWT claim.
 125   *
 126   * @return array
 127   */
 128  function lti_get_jwt_claim_mapping() {
 129      $mapping = [];
 130      $services = lti_get_services();
 131      foreach ($services as $service) {
 132          $mapping = array_merge($mapping, $service->get_jwt_claim_mappings());
 133      }
 134      $mapping = array_merge($mapping, [
 135          'accept_copy_advice' => [
 136              'suffix' => 'dl',
 137              'group' => 'deep_linking_settings',
 138              'claim' => 'accept_copy_advice',
 139              'isarray' => false,
 140              'type' => 'boolean'
 141          ],
 142          'accept_media_types' => [
 143              'suffix' => 'dl',
 144              'group' => 'deep_linking_settings',
 145              'claim' => 'accept_media_types',
 146              'isarray' => true
 147          ],
 148          'accept_multiple' => [
 149              'suffix' => 'dl',
 150              'group' => 'deep_linking_settings',
 151              'claim' => 'accept_multiple',
 152              'isarray' => false,
 153              'type' => 'boolean'
 154          ],
 155          'accept_presentation_document_targets' => [
 156              'suffix' => 'dl',
 157              'group' => 'deep_linking_settings',
 158              'claim' => 'accept_presentation_document_targets',
 159              'isarray' => true
 160          ],
 161          'accept_types' => [
 162              'suffix' => 'dl',
 163              'group' => 'deep_linking_settings',
 164              'claim' => 'accept_types',
 165              'isarray' => true
 166          ],
 167          'accept_unsigned' => [
 168              'suffix' => 'dl',
 169              'group' => 'deep_linking_settings',
 170              'claim' => 'accept_unsigned',
 171              'isarray' => false,
 172              'type' => 'boolean'
 173          ],
 174          'auto_create' => [
 175              'suffix' => 'dl',
 176              'group' => 'deep_linking_settings',
 177              'claim' => 'auto_create',
 178              'isarray' => false,
 179              'type' => 'boolean'
 180          ],
 181          'can_confirm' => [
 182              'suffix' => 'dl',
 183              'group' => 'deep_linking_settings',
 184              'claim' => 'can_confirm',
 185              'isarray' => false,
 186              'type' => 'boolean'
 187          ],
 188          'content_item_return_url' => [
 189              'suffix' => 'dl',
 190              'group' => 'deep_linking_settings',
 191              'claim' => 'deep_link_return_url',
 192              'isarray' => false
 193          ],
 194          'content_items' => [
 195              'suffix' => 'dl',
 196              'group' => '',
 197              'claim' => 'content_items',
 198              'isarray' => true
 199          ],
 200          'data' => [
 201              'suffix' => 'dl',
 202              'group' => 'deep_linking_settings',
 203              'claim' => 'data',
 204              'isarray' => false
 205          ],
 206          'text' => [
 207              'suffix' => 'dl',
 208              'group' => 'deep_linking_settings',
 209              'claim' => 'text',
 210              'isarray' => false
 211          ],
 212          'title' => [
 213              'suffix' => 'dl',
 214              'group' => 'deep_linking_settings',
 215              'claim' => 'title',
 216              'isarray' => false
 217          ],
 218          'lti_msg' => [
 219              'suffix' => 'dl',
 220              'group' => '',
 221              'claim' => 'msg',
 222              'isarray' => false
 223          ],
 224          'lti_log' => [
 225              'suffix' => 'dl',
 226              'group' => '',
 227              'claim' => 'log',
 228              'isarray' => false
 229          ],
 230          'lti_errormsg' => [
 231              'suffix' => 'dl',
 232              'group' => '',
 233              'claim' => 'errormsg',
 234              'isarray' => false
 235          ],
 236          'lti_errorlog' => [
 237              'suffix' => 'dl',
 238              'group' => '',
 239              'claim' => 'errorlog',
 240              'isarray' => false
 241          ],
 242          'context_id' => [
 243              'suffix' => '',
 244              'group' => 'context',
 245              'claim' => 'id',
 246              'isarray' => false
 247          ],
 248          'context_label' => [
 249              'suffix' => '',
 250              'group' => 'context',
 251              'claim' => 'label',
 252              'isarray' => false
 253          ],
 254          'context_title' => [
 255              'suffix' => '',
 256              'group' => 'context',
 257              'claim' => 'title',
 258              'isarray' => false
 259          ],
 260          'context_type' => [
 261              'suffix' => '',
 262              'group' => 'context',
 263              'claim' => 'type',
 264              'isarray' => true
 265          ],
 266          'for_user_id' => [
 267              'suffix' => '',
 268              'group' => 'for_user',
 269              'claim' => 'user_id',
 270              'isarray' => false
 271          ],
 272          'lis_course_offering_sourcedid' => [
 273              'suffix' => '',
 274              'group' => 'lis',
 275              'claim' => 'course_offering_sourcedid',
 276              'isarray' => false
 277          ],
 278          'lis_course_section_sourcedid' => [
 279              'suffix' => '',
 280              'group' => 'lis',
 281              'claim' => 'course_section_sourcedid',
 282              'isarray' => false
 283          ],
 284          'launch_presentation_css_url' => [
 285              'suffix' => '',
 286              'group' => 'launch_presentation',
 287              'claim' => 'css_url',
 288              'isarray' => false
 289          ],
 290          'launch_presentation_document_target' => [
 291              'suffix' => '',
 292              'group' => 'launch_presentation',
 293              'claim' => 'document_target',
 294              'isarray' => false
 295          ],
 296          'launch_presentation_height' => [
 297              'suffix' => '',
 298              'group' => 'launch_presentation',
 299              'claim' => 'height',
 300              'isarray' => false
 301          ],
 302          'launch_presentation_locale' => [
 303              'suffix' => '',
 304              'group' => 'launch_presentation',
 305              'claim' => 'locale',
 306              'isarray' => false
 307          ],
 308          'launch_presentation_return_url' => [
 309              'suffix' => '',
 310              'group' => 'launch_presentation',
 311              'claim' => 'return_url',
 312              'isarray' => false
 313          ],
 314          'launch_presentation_width' => [
 315              'suffix' => '',
 316              'group' => 'launch_presentation',
 317              'claim' => 'width',
 318              'isarray' => false
 319          ],
 320          'lis_person_contact_email_primary' => [
 321              'suffix' => '',
 322              'group' => null,
 323              'claim' => 'email',
 324              'isarray' => false
 325          ],
 326          'lis_person_name_family' => [
 327              'suffix' => '',
 328              'group' => null,
 329              'claim' => 'family_name',
 330              'isarray' => false
 331          ],
 332          'lis_person_name_full' => [
 333              'suffix' => '',
 334              'group' => null,
 335              'claim' => 'name',
 336              'isarray' => false
 337          ],
 338          'lis_person_name_given' => [
 339              'suffix' => '',
 340              'group' => null,
 341              'claim' => 'given_name',
 342              'isarray' => false
 343          ],
 344          'lis_person_sourcedid' => [
 345              'suffix' => '',
 346              'group' => 'lis',
 347              'claim' => 'person_sourcedid',
 348              'isarray' => false
 349          ],
 350          'user_id' => [
 351              'suffix' => '',
 352              'group' => null,
 353              'claim' => 'sub',
 354              'isarray' => false
 355          ],
 356          'user_image' => [
 357              'suffix' => '',
 358              'group' => null,
 359              'claim' => 'picture',
 360              'isarray' => false
 361          ],
 362          'roles' => [
 363              'suffix' => '',
 364              'group' => '',
 365              'claim' => 'roles',
 366              'isarray' => true
 367          ],
 368          'role_scope_mentor' => [
 369              'suffix' => '',
 370              'group' => '',
 371              'claim' => 'role_scope_mentor',
 372              'isarray' => false
 373          ],
 374          'deployment_id' => [
 375              'suffix' => '',
 376              'group' => '',
 377              'claim' => 'deployment_id',
 378              'isarray' => false
 379          ],
 380          'lti_message_type' => [
 381              'suffix' => '',
 382              'group' => '',
 383              'claim' => 'message_type',
 384              'isarray' => false
 385          ],
 386          'lti_version' => [
 387              'suffix' => '',
 388              'group' => '',
 389              'claim' => 'version',
 390              'isarray' => false
 391          ],
 392          'resource_link_description' => [
 393              'suffix' => '',
 394              'group' => 'resource_link',
 395              'claim' => 'description',
 396              'isarray' => false
 397          ],
 398          'resource_link_id' => [
 399              'suffix' => '',
 400              'group' => 'resource_link',
 401              'claim' => 'id',
 402              'isarray' => false
 403          ],
 404          'resource_link_title' => [
 405              'suffix' => '',
 406              'group' => 'resource_link',
 407              'claim' => 'title',
 408              'isarray' => false
 409          ],
 410          'tool_consumer_info_product_family_code' => [
 411              'suffix' => '',
 412              'group' => 'tool_platform',
 413              'claim' => 'product_family_code',
 414              'isarray' => false
 415          ],
 416          'tool_consumer_info_version' => [
 417              'suffix' => '',
 418              'group' => 'tool_platform',
 419              'claim' => 'version',
 420              'isarray' => false
 421          ],
 422          'tool_consumer_instance_contact_email' => [
 423              'suffix' => '',
 424              'group' => 'tool_platform',
 425              'claim' => 'contact_email',
 426              'isarray' => false
 427          ],
 428          'tool_consumer_instance_description' => [
 429              'suffix' => '',
 430              'group' => 'tool_platform',
 431              'claim' => 'description',
 432              'isarray' => false
 433          ],
 434          'tool_consumer_instance_guid' => [
 435              'suffix' => '',
 436              'group' => 'tool_platform',
 437              'claim' => 'guid',
 438              'isarray' => false
 439          ],
 440          'tool_consumer_instance_name' => [
 441              'suffix' => '',
 442              'group' => 'tool_platform',
 443              'claim' => 'name',
 444              'isarray' => false
 445          ],
 446          'tool_consumer_instance_url' => [
 447              'suffix' => '',
 448              'group' => 'tool_platform',
 449              'claim' => 'url',
 450              'isarray' => false
 451          ]
 452      ]);
 453      return $mapping;
 454  }
 455  
 456  /**
 457   * Return the type of the instance, using domain matching if no explicit type is set.
 458   *
 459   * @param  object $instance the external tool activity settings
 460   * @return object|null
 461   * @since  Moodle 3.9
 462   */
 463  function lti_get_instance_type(object $instance) : ?object {
 464      if (empty($instance->typeid)) {
 465          if (!$tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course)) {
 466              $tool = lti_get_tool_by_url_match($instance->securetoolurl,  $instance->course);
 467          }
 468          return $tool;
 469      }
 470      return lti_get_type($instance->typeid);
 471  }
 472  
 473  /**
 474   * Return the launch data required for opening the external tool.
 475   *
 476   * @param  stdClass $instance the external tool activity settings
 477   * @param  string $nonce  the nonce value to use (applies to LTI 1.3 only)
 478   * @return array the endpoint URL and parameters (including the signature)
 479   * @since  Moodle 3.0
 480   */
 481  function lti_get_launch_data($instance, $nonce = '', $messagetype = 'basic-lti-launch-request', $foruserid = 0) {
 482      global $PAGE, $USER;
 483      $messagetype = $messagetype ? $messagetype : 'basic-lti-launch-request';
 484      $tool = lti_get_instance_type($instance);
 485      if ($tool) {
 486          $typeid = $tool->id;
 487          $ltiversion = $tool->ltiversion;
 488      } else {
 489          $typeid = null;
 490          $ltiversion = LTI_VERSION_1;
 491      }
 492  
 493      if ($typeid) {
 494          $typeconfig = lti_get_type_config($typeid);
 495      } else {
 496          // There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults.
 497          $typeconfig = (array)$instance;
 498  
 499          $typeconfig['sendname'] = $instance->instructorchoicesendname;
 500          $typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr;
 501          $typeconfig['customparameters'] = $instance->instructorcustomparameters;
 502          $typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades;
 503          $typeconfig['allowroster'] = $instance->instructorchoiceallowroster;
 504          $typeconfig['forcessl'] = '0';
 505      }
 506  
 507      if (isset($tool->toolproxyid)) {
 508          $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
 509          $key = $toolproxy->guid;
 510          $secret = $toolproxy->secret;
 511      } else {
 512          $toolproxy = null;
 513          if (!empty($instance->resourcekey)) {
 514              $key = $instance->resourcekey;
 515          } else if ($ltiversion === LTI_VERSION_1P3) {
 516              $key = $tool->clientid;
 517          } else if (!empty($typeconfig['resourcekey'])) {
 518              $key = $typeconfig['resourcekey'];
 519          } else {
 520              $key = '';
 521          }
 522          if (!empty($instance->password)) {
 523              $secret = $instance->password;
 524          } else if (!empty($typeconfig['password'])) {
 525              $secret = $typeconfig['password'];
 526          } else {
 527              $secret = '';
 528          }
 529      }
 530  
 531      $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl'];
 532      $endpoint = trim($endpoint);
 533  
 534      // If the current request is using SSL and a secure tool URL is specified, use it.
 535      if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) {
 536          $endpoint = trim($instance->securetoolurl);
 537      }
 538  
 539      // If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL.
 540      if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
 541          if (!empty($instance->securetoolurl)) {
 542              $endpoint = trim($instance->securetoolurl);
 543          }
 544  
 545          if ($endpoint !== '') {
 546              $endpoint = lti_ensure_url_is_https($endpoint);
 547          }
 548      } else if ($endpoint !== '' && !strstr($endpoint, '://')) {
 549          $endpoint = 'http://' . $endpoint;
 550      }
 551  
 552      $orgid = lti_get_organizationid($typeconfig);
 553  
 554      $course = $PAGE->course;
 555      $islti2 = isset($tool->toolproxyid);
 556      $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2, $messagetype, $foruserid);
 557      if ($islti2) {
 558          $requestparams = lti_build_request_lti2($tool, $allparams);
 559      } else {
 560          $requestparams = $allparams;
 561      }
 562      $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype));
 563      $customstr = '';
 564      if (isset($typeconfig['customparameters'])) {
 565          $customstr = $typeconfig['customparameters'];
 566      }
 567      $services = lti_get_services();
 568      foreach ($services as $service) {
 569          [$endpoint, $customstr] = $service->override_endpoint($messagetype,
 570              $endpoint, $customstr, $instance->course, $instance);
 571      }
 572      $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr,
 573          $instance->instructorcustomparameters, $islti2));
 574  
 575      $launchcontainer = lti_get_launch_container($instance, $typeconfig);
 576      $returnurlparams = array('course' => $course->id,
 577          'launch_container' => $launchcontainer,
 578          'instanceid' => $instance->id,
 579          'sesskey' => sesskey());
 580  
 581      // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns.
 582      $url = new \moodle_url('/mod/lti/return.php', $returnurlparams);
 583      $returnurl = $url->out(false);
 584  
 585      if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
 586          $returnurl = lti_ensure_url_is_https($returnurl);
 587      }
 588  
 589      $target = '';
 590      switch($launchcontainer) {
 591          case LTI_LAUNCH_CONTAINER_EMBED:
 592          case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS:
 593              $target = 'iframe';
 594              break;
 595          case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW:
 596              $target = 'frame';
 597              break;
 598          case LTI_LAUNCH_CONTAINER_WINDOW:
 599              $target = 'window';
 600              break;
 601      }
 602      if (!empty($target)) {
 603          $requestparams['launch_presentation_document_target'] = $target;
 604      }
 605  
 606      $requestparams['launch_presentation_return_url'] = $returnurl;
 607  
 608      // Add the parameters configured by the LTI services.
 609      if ($typeid && !$islti2) {
 610          $services = lti_get_services();
 611          foreach ($services as $service) {
 612              $serviceparameters = $service->get_launch_parameters('basic-lti-launch-request',
 613                      $course->id, $USER->id , $typeid, $instance->id);
 614              foreach ($serviceparameters as $paramkey => $paramvalue) {
 615                  $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
 616                      $islti2);
 617              }
 618          }
 619      }
 620  
 621      // Allow request params to be updated by sub-plugins.
 622      $plugins = core_component::get_plugin_list('ltisource');
 623      foreach (array_keys($plugins) as $plugin) {
 624          $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch',
 625              array($instance, $endpoint, $requestparams), array());
 626  
 627          if (!empty($pluginparams) && is_array($pluginparams)) {
 628              $requestparams = array_merge($requestparams, $pluginparams);
 629          }
 630      }
 631  
 632      if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) {
 633          if ($ltiversion !== LTI_VERSION_1P3) {
 634              $parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret);
 635          } else {
 636              $parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce);
 637          }
 638  
 639          $endpointurl = new \moodle_url($endpoint);
 640          $endpointparams = $endpointurl->params();
 641  
 642          // Strip querystring params in endpoint url from $parms to avoid duplication.
 643          if (!empty($endpointparams) && !empty($parms)) {
 644              foreach (array_keys($endpointparams) as $paramname) {
 645                  if (isset($parms[$paramname])) {
 646                      unset($parms[$paramname]);
 647                  }
 648              }
 649          }
 650  
 651      } else {
 652          // If no key and secret, do the launch unsigned.
 653          $returnurlparams['unsigned'] = '1';
 654          $parms = $requestparams;
 655      }
 656  
 657      return array($endpoint, $parms);
 658  }
 659  
 660  /**
 661   * Launch an external tool activity.
 662   *
 663   * @param stdClass $instance the external tool activity settings
 664   * @param int $foruserid for user param, optional
 665   * @return string The HTML code containing the javascript code for the launch
 666   */
 667  function lti_launch_tool($instance, $foruserid=0) {
 668  
 669      list($endpoint, $parms) = lti_get_launch_data($instance, '', '', $foruserid);
 670      $debuglaunch = ( $instance->debuglaunch == 1 );
 671  
 672      $content = lti_post_launch_html($parms, $endpoint, $debuglaunch);
 673  
 674      echo $content;
 675  }
 676  
 677  /**
 678   * Prepares an LTI registration request message
 679   *
 680   * @param object $toolproxy  Tool Proxy instance object
 681   */
 682  function lti_register($toolproxy) {
 683      $endpoint = $toolproxy->regurl;
 684  
 685      // Change the status to pending.
 686      $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING;
 687      lti_update_tool_proxy($toolproxy);
 688  
 689      $requestparams = lti_build_registration_request($toolproxy);
 690  
 691      $content = lti_post_launch_html($requestparams, $endpoint, false);
 692  
 693      echo $content;
 694  }
 695  
 696  
 697  /**
 698   * Gets the parameters for the regirstration request
 699   *
 700   * @param object $toolproxy Tool Proxy instance object
 701   * @return array Registration request parameters
 702   */
 703  function lti_build_registration_request($toolproxy) {
 704      $key = $toolproxy->guid;
 705      $secret = $toolproxy->secret;
 706  
 707      $requestparams = array();
 708      $requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest';
 709      $requestparams['lti_version'] = 'LTI-2p0';
 710      $requestparams['reg_key'] = $key;
 711      $requestparams['reg_password'] = $secret;
 712      $requestparams['reg_url'] = $toolproxy->regurl;
 713  
 714      // Add the profile URL.
 715      $profileservice = lti_get_service_by_name('profile');
 716      $profileservice->set_tool_proxy($toolproxy);
 717      $requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url');
 718  
 719      // Add the return URL.
 720      $returnurlparams = array('id' => $toolproxy->id, 'sesskey' => sesskey());
 721      $url = new \moodle_url('/mod/lti/externalregistrationreturn.php', $returnurlparams);
 722      $returnurl = $url->out(false);
 723  
 724      $requestparams['launch_presentation_return_url'] = $returnurl;
 725  
 726      return $requestparams;
 727  }
 728  
 729  
 730  /** get Organization ID using default if no value provided
 731   * @param object $typeconfig
 732   * @return string
 733   */
 734  function lti_get_organizationid($typeconfig) {
 735      global $CFG;
 736      // Default the organizationid if not specified.
 737      if (empty($typeconfig['organizationid'])) {
 738          if (($typeconfig['organizationid_default'] ?? LTI_DEFAULT_ORGID_SITEHOST) == LTI_DEFAULT_ORGID_SITEHOST) {
 739              $urlparts = parse_url($CFG->wwwroot);
 740              return $urlparts['host'];
 741          } else {
 742              return md5(get_site_identifier());
 743          }
 744      }
 745      return $typeconfig['organizationid'];
 746  }
 747  
 748  /**
 749   * Build source ID
 750   *
 751   * @param int $instanceid
 752   * @param int $userid
 753   * @param string $servicesalt
 754   * @param null|int $typeid
 755   * @param null|int $launchid
 756   * @return stdClass
 757   */
 758  function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) {
 759      $data = new \stdClass();
 760  
 761      $data->instanceid = $instanceid;
 762      $data->userid = $userid;
 763      $data->typeid = $typeid;
 764      if (!empty($launchid)) {
 765          $data->launchid = $launchid;
 766      } else {
 767          $data->launchid = mt_rand();
 768      }
 769  
 770      $json = json_encode($data);
 771  
 772      $hash = hash('sha256', $json . $servicesalt, false);
 773  
 774      $container = new \stdClass();
 775      $container->data = $data;
 776      $container->hash = $hash;
 777  
 778      return $container;
 779  }
 780  
 781  /**
 782   * This function builds the request that must be sent to the tool producer
 783   *
 784   * @param object    $instance       Basic LTI instance object
 785   * @param array     $typeconfig     Basic LTI tool configuration
 786   * @param object    $course         Course object
 787   * @param int|null  $typeid         Basic LTI tool ID
 788   * @param boolean   $islti2         True if an LTI 2 tool is being launched
 789   * @param string    $messagetype    LTI Message Type for this launch
 790   * @param int       $foruserid      User targeted by this launch
 791   *
 792   * @return array                    Request details
 793   */
 794  function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false,
 795      $messagetype = 'basic-lti-launch-request', $foruserid = 0) {
 796      global $USER, $CFG;
 797  
 798      if (empty($instance->cmid)) {
 799          $instance->cmid = 0;
 800      }
 801  
 802      $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2);
 803  
 804      $requestparams = array(
 805          'user_id' => $USER->id,
 806          'lis_person_sourcedid' => $USER->idnumber,
 807          'roles' => $role,
 808          'context_id' => $course->id,
 809          'context_label' => trim(html_to_text($course->shortname, 0)),
 810          'context_title' => trim(html_to_text($course->fullname, 0)),
 811      );
 812      if ($foruserid) {
 813          $requestparams['for_user_id'] = $foruserid;
 814      }
 815      if ($messagetype) {
 816          $requestparams['lti_message_type'] = $messagetype;
 817      }
 818      if (!empty($instance->name)) {
 819          $requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0));
 820      }
 821      if (!empty($instance->cmid)) {
 822          $intro = format_module_intro('lti', $instance, $instance->cmid);
 823          $intro = trim(html_to_text($intro, 0, false));
 824  
 825          // This may look weird, but this is required for new lines
 826          // so we generate the same OAuth signature as the tool provider.
 827          $intro = str_replace("\n", "\r\n", $intro);
 828          $requestparams['resource_link_description'] = $intro;
 829      }
 830      if (!empty($instance->id)) {
 831          $requestparams['resource_link_id'] = $instance->id;
 832      }
 833      if (!empty($instance->resource_link_id)) {
 834          $requestparams['resource_link_id'] = $instance->resource_link_id;
 835      }
 836      if ($course->format == 'site') {
 837          $requestparams['context_type'] = 'Group';
 838      } else {
 839          $requestparams['context_type'] = 'CourseSection';
 840          $requestparams['lis_course_section_sourcedid'] = $course->idnumber;
 841      }
 842  
 843      if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 ||
 844              $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS ||
 845              ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))
 846      ) {
 847          $placementsecret = $instance->servicesalt;
 848          $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid));
 849          $requestparams['lis_result_sourcedid'] = $sourcedid;
 850  
 851          // Add outcome service URL.
 852          $serviceurl = new \moodle_url('/mod/lti/service.php');
 853          $serviceurl = $serviceurl->out();
 854  
 855          $forcessl = false;
 856          if (!empty($CFG->mod_lti_forcessl)) {
 857              $forcessl = true;
 858          }
 859  
 860          if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) {
 861              $serviceurl = lti_ensure_url_is_https($serviceurl);
 862          }
 863  
 864          $requestparams['lis_outcome_service_url'] = $serviceurl;
 865      }
 866  
 867      // Send user's name and email data if appropriate.
 868      if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS ||
 869          ($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname)
 870              && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS)
 871      ) {
 872          $requestparams['lis_person_name_given'] = $USER->firstname;
 873          $requestparams['lis_person_name_family'] = $USER->lastname;
 874          $requestparams['lis_person_name_full'] = fullname($USER);
 875          $requestparams['ext_user_username'] = $USER->username;
 876      }
 877  
 878      if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS ||
 879          ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr)
 880              && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS)
 881      ) {
 882          $requestparams['lis_person_contact_email_primary'] = $USER->email;
 883      }
 884  
 885      return $requestparams;
 886  }
 887  
 888  /**
 889   * This function builds the request that must be sent to an LTI 2 tool provider
 890   *
 891   * @param object    $tool           Basic LTI tool object
 892   * @param array     $params         Custom launch parameters
 893   *
 894   * @return array                    Request details
 895   */
 896  function lti_build_request_lti2($tool, $params) {
 897  
 898      $requestparams = array();
 899  
 900      $capabilities = lti_get_capabilities();
 901      $enabledcapabilities = explode("\n", $tool->enabledcapability);
 902      foreach ($enabledcapabilities as $capability) {
 903          if (array_key_exists($capability, $capabilities)) {
 904              $val = $capabilities[$capability];
 905              if ($val && (substr($val, 0, 1) != '$')) {
 906                  if (isset($params[$val])) {
 907                      $requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]];
 908                  }
 909              }
 910          }
 911      }
 912  
 913      return $requestparams;
 914  
 915  }
 916  
 917  /**
 918   * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer
 919   *
 920   * @param stdClass  $instance       Basic LTI instance object
 921   * @param string    $orgid          Organisation ID
 922   * @param boolean   $islti2         True if an LTI 2 tool is being launched
 923   * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
 924   *
 925   * @return array                    Request details
 926   * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
 927   * @see lti_build_standard_message()
 928   */
 929  function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') {
 930      if (!$islti2) {
 931          $ltiversion = LTI_VERSION_1;
 932      } else {
 933          $ltiversion = LTI_VERSION_2;
 934      }
 935      return lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype);
 936  }
 937  
 938  /**
 939   * This function builds the standard parameters for an LTI message that must be sent to the tool producer
 940   *
 941   * @param stdClass  $instance       Basic LTI instance object
 942   * @param string    $orgid          Organisation ID
 943   * @param boolean   $ltiversion     LTI version to be used for tool messages
 944   * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
 945   *
 946   * @return array                    Message parameters
 947   */
 948  function lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype = 'basic-lti-launch-request') {
 949      global $CFG;
 950  
 951      $requestparams = array();
 952  
 953      if ($instance) {
 954          $requestparams['resource_link_id'] = $instance->id;
 955          if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) {
 956              $requestparams['resource_link_id'] = $instance->resource_link_id;
 957          }
 958      }
 959  
 960      $requestparams['launch_presentation_locale'] = current_language();
 961  
 962      // Make sure we let the tool know what LMS they are being called from.
 963      $requestparams['ext_lms'] = 'moodle-2';
 964      $requestparams['tool_consumer_info_product_family_code'] = 'moodle';
 965      $requestparams['tool_consumer_info_version'] = strval($CFG->version);
 966  
 967      // Add oauth_callback to be compliant with the 1.0A spec.
 968      $requestparams['oauth_callback'] = 'about:blank';
 969  
 970      $requestparams['lti_version'] = $ltiversion;
 971      $requestparams['lti_message_type'] = $messagetype;
 972  
 973      if ($orgid) {
 974          $requestparams["tool_consumer_instance_guid"] = $orgid;
 975      }
 976      if (!empty($CFG->mod_lti_institution_name)) {
 977          $requestparams['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0));
 978      } else {
 979          $requestparams['tool_consumer_instance_name'] = get_site()->shortname;
 980      }
 981      $requestparams['tool_consumer_instance_description'] = trim(html_to_text(get_site()->fullname, 0));
 982  
 983      return $requestparams;
 984  }
 985  
 986  /**
 987   * This function builds the custom parameters
 988   *
 989   * @param object    $toolproxy      Tool proxy instance object
 990   * @param object    $tool           Tool instance object
 991   * @param object    $instance       Tool placement instance object
 992   * @param array     $params         LTI launch parameters
 993   * @param string    $customstr      Custom parameters defined for tool
 994   * @param string    $instructorcustomstr      Custom parameters defined for this placement
 995   * @param boolean   $islti2         True if an LTI 2 tool is being launched
 996   *
 997   * @return array                    Custom parameters
 998   */
 999  function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) {
1000  
1001      // Concatenate the custom parameters from the administrator and the instructor
1002      // Instructor parameters are only taken into consideration if the administrator
1003      // has given permission.
1004      $custom = array();
1005      if ($customstr) {
1006          $custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2);
1007      }
1008      if ($instructorcustomstr) {
1009          $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
1010              $instructorcustomstr, $islti2), $custom);
1011      }
1012      if ($islti2) {
1013          $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
1014              $tool->parameter, true), $custom);
1015          $settings = lti_get_tool_settings($tool->toolproxyid);
1016          $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1017          if (!empty($instance->course)) {
1018              $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course);
1019              $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1020              if (!empty($instance->id)) {
1021                  $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id);
1022                  $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1023              }
1024          }
1025      }
1026  
1027      return $custom;
1028  }
1029  
1030  /**
1031   * Builds a standard LTI Content-Item selection request.
1032   *
1033   * @param int $id The tool type ID.
1034   * @param stdClass $course The course object.
1035   * @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP)
1036   *                              will use to return the Content-Item message.
1037   * @param string $title The tool's title, if available.
1038   * @param string $text The text to display to represent the content item. This value may be a long description of the content item.
1039   * @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default.
1040   * @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened
1041   *                                   (via the presentationDocumentTarget element for a returned content item).
1042   *                                   If empty, "frame", "iframe", and "window" will be supported by default.
1043   * @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without
1044   * @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default.
1045   *                         any option for the user to cancel the operation. False by default.
1046   * @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not.
1047   *                       A signed message should always be required when the content item is being created automatically in the
1048   *                       TC without further interaction from the user. False by default.
1049   * @param bool $canconfirm Flag for can_confirm parameter. False by default.
1050   * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default.
1051   * @param string $nonce
1052   * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface.
1053   * @throws moodle_exception When the LTI tool type does not exist.`
1054   * @throws coding_exception For invalid media type and presentation target parameters.
1055   */
1056  function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
1057                                                    $presentationtargets = [], $autocreate = false, $multiple = true,
1058                                                    $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {
1059      global $USER;
1060  
1061      $tool = lti_get_type($id);
1062      // Validate parameters.
1063      if (!$tool) {
1064          throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1065      }
1066      if (!is_array($mediatypes)) {
1067          throw new coding_exception('The list of accepted media types should be in an array');
1068      }
1069      if (!is_array($presentationtargets)) {
1070          throw new coding_exception('The list of accepted presentation targets should be in an array');
1071      }
1072  
1073      // Check title. If empty, use the tool's name.
1074      if (empty($title)) {
1075          $title = $tool->name;
1076      }
1077  
1078      $typeconfig = lti_get_type_config($id);
1079      $key = '';
1080      $secret = '';
1081      $islti2 = false;
1082      $islti13 = false;
1083      if (isset($tool->toolproxyid)) {
1084          $islti2 = true;
1085          $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
1086          $key = $toolproxy->guid;
1087          $secret = $toolproxy->secret;
1088      } else {
1089          $islti13 = $tool->ltiversion === LTI_VERSION_1P3;
1090          $toolproxy = null;
1091          if ($islti13 && !empty($tool->clientid)) {
1092              $key = $tool->clientid;
1093          } else if (!$islti13 && !empty($typeconfig['resourcekey'])) {
1094              $key = $typeconfig['resourcekey'];
1095          }
1096          if (!empty($typeconfig['password'])) {
1097              $secret = $typeconfig['password'];
1098          }
1099      }
1100      $tool->enabledcapability = '';
1101      if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) {
1102          $tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest'];
1103      }
1104  
1105      $tool->parameter = '';
1106      if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) {
1107          $tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest'];
1108      }
1109  
1110      // Set the tool URL.
1111      if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) {
1112          $toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']);
1113      } else {
1114          $toolurl = new moodle_url($typeconfig['toolurl']);
1115      }
1116  
1117      // Check if SSL is forced.
1118      if (!empty($typeconfig['forcessl'])) {
1119          // Make sure the tool URL is set to https.
1120          if (strtolower($toolurl->get_scheme()) === 'http') {
1121              $toolurl->set_scheme('https');
1122          }
1123          // Make sure the return URL is set to https.
1124          if (strtolower($returnurl->get_scheme()) === 'http') {
1125              $returnurl->set_scheme('https');
1126          }
1127      }
1128      $toolurlout = $toolurl->out(false);
1129  
1130      // Get base request parameters.
1131      $instance = new stdClass();
1132      $instance->course = $course->id;
1133      $requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2);
1134  
1135      // Get LTI2-specific request parameters and merge to the request parameters if applicable.
1136      if ($islti2) {
1137          $lti2params = lti_build_request_lti2($tool, $requestparams);
1138          $requestparams = array_merge($requestparams, $lti2params);
1139      }
1140  
1141      // Get standard request parameters and merge to the request parameters.
1142      $orgid = lti_get_organizationid($typeconfig);
1143      $standardparams = lti_build_standard_message(null, $orgid, $tool->ltiversion, 'ContentItemSelectionRequest');
1144      $requestparams = array_merge($requestparams, $standardparams);
1145  
1146      // Get custom request parameters and merge to the request parameters.
1147      $customstr = '';
1148      if (!empty($typeconfig['customparameters'])) {
1149          $customstr = $typeconfig['customparameters'];
1150      }
1151      $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2);
1152      $requestparams = array_merge($requestparams, $customparams);
1153  
1154      // Add the parameters configured by the LTI services.
1155      if ($id && !$islti2) {
1156          $services = lti_get_services();
1157          foreach ($services as $service) {
1158              $serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
1159                  $course->id, $USER->id , $id);
1160              foreach ($serviceparameters as $paramkey => $paramvalue) {
1161                  $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
1162                      $islti2);
1163              }
1164          }
1165      }
1166  
1167      // Allow request params to be updated by sub-plugins.
1168      $plugins = core_component::get_plugin_list('ltisource');
1169      foreach (array_keys($plugins) as $plugin) {
1170          $pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []);
1171  
1172          if (!empty($pluginparams) && is_array($pluginparams)) {
1173              $requestparams = array_merge($requestparams, $pluginparams);
1174          }
1175      }
1176  
1177      if (!$islti13) {
1178          // Media types. Set to ltilink by default if empty.
1179          if (empty($mediatypes)) {
1180              $mediatypes = [
1181                  'application/vnd.ims.lti.v1.ltilink',
1182              ];
1183          }
1184          $requestparams['accept_media_types'] = implode(',', $mediatypes);
1185      } else {
1186          // Only LTI links are currently supported.
1187          $requestparams['accept_types'] = 'ltiResourceLink';
1188      }
1189  
1190      // Presentation targets. Supports frame, iframe, window by default if empty.
1191      if (empty($presentationtargets)) {
1192          $presentationtargets = [
1193              'frame',
1194              'iframe',
1195              'window',
1196          ];
1197      }
1198      $requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets);
1199  
1200      // Other request parameters.
1201      $requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false';
1202      $requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false';
1203      $requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false';
1204      $requestparams['auto_create'] = $autocreate === true ? 'true' : 'false';
1205      $requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false';
1206      $requestparams['content_item_return_url'] = $returnurl->out(false);
1207      $requestparams['title'] = $title;
1208      $requestparams['text'] = $text;
1209      if (!$islti13) {
1210          $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret);
1211      } else {
1212          $signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce);
1213      }
1214      $toolurlparams = $toolurl->params();
1215  
1216      // Strip querystring params in endpoint url from $signedparams to avoid duplication.
1217      if (!empty($toolurlparams) && !empty($signedparams)) {
1218          foreach (array_keys($toolurlparams) as $paramname) {
1219              if (isset($signedparams[$paramname])) {
1220                  unset($signedparams[$paramname]);
1221              }
1222          }
1223      }
1224  
1225      // Check for params that should not be passed. Unset if they are set.
1226      $unwantedparams = [
1227          'resource_link_id',
1228          'resource_link_title',
1229          'resource_link_description',
1230          'launch_presentation_return_url',
1231          'lis_result_sourcedid',
1232      ];
1233      foreach ($unwantedparams as $param) {
1234          if (isset($signedparams[$param])) {
1235              unset($signedparams[$param]);
1236          }
1237      }
1238  
1239      // Prepare result object.
1240      $result = new stdClass();
1241      $result->params = $signedparams;
1242      $result->url = $toolurlout;
1243  
1244      return $result;
1245  }
1246  
1247  /**
1248   * Verifies the OAuth signature of an incoming message.
1249   *
1250   * @param int $typeid The tool type ID.
1251   * @param string $consumerkey The consumer key.
1252   * @return stdClass Tool type
1253   * @throws moodle_exception
1254   * @throws lti\OAuthException
1255   */
1256  function lti_verify_oauth_signature($typeid, $consumerkey) {
1257      $tool = lti_get_type($typeid);
1258      // Validate parameters.
1259      if (!$tool) {
1260          throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1261      }
1262      $typeconfig = lti_get_type_config($typeid);
1263  
1264      if (isset($tool->toolproxyid)) {
1265          $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
1266          $key = $toolproxy->guid;
1267          $secret = $toolproxy->secret;
1268      } else {
1269          $toolproxy = null;
1270          if (!empty($typeconfig['resourcekey'])) {
1271              $key = $typeconfig['resourcekey'];
1272          } else {
1273              $key = '';
1274          }
1275          if (!empty($typeconfig['password'])) {
1276              $secret = $typeconfig['password'];
1277          } else {
1278              $secret = '';
1279          }
1280      }
1281  
1282      if ($consumerkey !== $key) {
1283          throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1284      }
1285  
1286      $store = new lti\TrivialOAuthDataStore();
1287      $store->add_consumer($key, $secret);
1288      $server = new lti\OAuthServer($store);
1289      $method = new lti\OAuthSignatureMethod_HMAC_SHA1();
1290      $server->add_signature_method($method);
1291      $request = lti\OAuthRequest::from_request();
1292      try {
1293          $server->verify_request($request);
1294      } catch (lti\OAuthException $e) {
1295          throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage());
1296      }
1297  
1298      return $tool;
1299  }
1300  
1301  /**
1302   * Verifies the JWT signature using a JWK keyset.
1303   *
1304   * @param string $jwtparam JWT parameter value.
1305   * @param string $keyseturl The tool keyseturl.
1306   * @param string $clientid The tool client id.
1307   *
1308   * @return object The JWT's payload as a PHP object
1309   * @throws moodle_exception
1310   * @throws UnexpectedValueException     Provided JWT was invalid
1311   * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
1312   * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1313   * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
1314   * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
1315   */
1316  function lti_verify_with_keyset($jwtparam, $keyseturl, $clientid) {
1317      // Attempts to retrieve cached keyset.
1318      $cache = cache::make('mod_lti', 'keyset');
1319      $keyset = $cache->get($clientid);
1320  
1321      try {
1322          if (empty($keyset)) {
1323              throw new moodle_exception('errornocachedkeysetfound', 'mod_lti');
1324          }
1325          $keysetarr = json_decode($keyset, true);
1326          // JWK::parseKeySet uses RS256 algorithm by default.
1327          $keys = JWK::parseKeySet($keysetarr);
1328          $jwt = JWT::decode($jwtparam, $keys);
1329      } catch (Exception $e) {
1330          // Something went wrong, so attempt to update cached keyset and then try again.
1331          $keyset = download_file_content($keyseturl);
1332          $keysetarr = json_decode($keyset, true);
1333  
1334          // Fix for firebase/php-jwt's dependency on the optional 'alg' property in the JWK.
1335          $keysetarr = jwks_helper::fix_jwks_alg($keysetarr, $jwtparam);
1336  
1337          // JWK::parseKeySet uses RS256 algorithm by default.
1338          $keys = JWK::parseKeySet($keysetarr);
1339          $jwt = JWT::decode($jwtparam, $keys);
1340          // If sucessful, updates the cached keyset.
1341          $cache->set($clientid, $keyset);
1342      }
1343      return $jwt;
1344  }
1345  
1346  /**
1347   * Verifies the JWT signature of an incoming message.
1348   *
1349   * @param int $typeid The tool type ID.
1350   * @param string $consumerkey The consumer key.
1351   * @param string $jwtparam JWT parameter value
1352   *
1353   * @return stdClass Tool type
1354   * @throws moodle_exception
1355   * @throws UnexpectedValueException     Provided JWT was invalid
1356   * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
1357   * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1358   * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
1359   * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
1360   */
1361  function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
1362      $tool = lti_get_type($typeid);
1363  
1364      // Validate parameters.
1365      if (!$tool) {
1366          throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1367      }
1368      if (isset($tool->toolproxyid)) {
1369          throw new moodle_exception('JWT security not supported with LTI 2');
1370      }
1371  
1372      $typeconfig = lti_get_type_config($typeid);
1373  
1374      $key = $tool->clientid ?? '';
1375  
1376      if ($consumerkey !== $key) {
1377          throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1378      }
1379  
1380      if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) {
1381          $publickey = $typeconfig['publickey'] ?? '';
1382          if (empty($publickey)) {
1383              throw new moodle_exception('No public key configured');
1384          }
1385          // Attemps to verify jwt with RSA key.
1386          JWT::decode($jwtparam, new Key($publickey, 'RS256'));
1387      } else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) {
1388          $keyseturl = $typeconfig['publickeyset'] ?? '';
1389          if (empty($keyseturl)) {
1390              throw new moodle_exception('No public keyset configured');
1391          }
1392          // Attempts to verify jwt with jwk keyset.
1393          lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid);
1394      } else {
1395          throw new moodle_exception('Invalid public key type');
1396      }
1397  
1398      return $tool;
1399  }
1400  
1401  /**
1402   * Converts an array of custom parameters to a new line separated string.
1403   *
1404   * @param object $params list of params to concatenate
1405   *
1406   * @return string
1407   */
1408  function params_to_string(object $params) {
1409      $customparameters = [];
1410      foreach ($params as $key => $value) {
1411          $customparameters[] = "{$key}={$value}";
1412      }
1413      return implode("\n", $customparameters);
1414  }
1415  
1416  /**
1417   * Converts LTI 1.1 Content Item for LTI Link to Form data.
1418   *
1419   * @param object $tool Tool for which the item is created for.
1420   * @param object $typeconfig The tool configuration.
1421   * @param object $item Item populated from JSON to be converted to Form form
1422   *
1423   * @return stdClass Form config for the item
1424   */
1425  function content_item_to_form(object $tool, object $typeconfig, object $item) : stdClass {
1426      global $OUTPUT;
1427  
1428      $config = new stdClass();
1429      $config->name = '';
1430      if (isset($item->title)) {
1431          $config->name = $item->title;
1432      }
1433      if (empty($config->name)) {
1434          $config->name = $tool->name;
1435      }
1436      if (isset($item->text)) {
1437          $config->introeditor = [
1438              'text' => $item->text,
1439              'format' => FORMAT_PLAIN
1440          ];
1441      } else {
1442          $config->introeditor = [
1443              'text' => '',
1444              'format' => FORMAT_PLAIN
1445          ];
1446      }
1447      if (isset($item->icon->{'@id'})) {
1448          $iconurl = new moodle_url($item->icon->{'@id'});
1449          // Assign item's icon URL to secureicon or icon depending on its scheme.
1450          if (strtolower($iconurl->get_scheme()) === 'https') {
1451              $config->secureicon = $iconurl->out(false);
1452          } else {
1453              $config->icon = $iconurl->out(false);
1454          }
1455      }
1456      if (isset($item->url)) {
1457          $url = new moodle_url($item->url);
1458          $config->toolurl = $url->out(false);
1459          $config->typeid = 0;
1460      } else {
1461          $config->typeid = $tool->id;
1462      }
1463      $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
1464      $islti2 = $tool->ltiversion === LTI_VERSION_2;
1465      if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
1466          $acceptgrades = $typeconfig->lti_acceptgrades;
1467          if ($acceptgrades == LTI_SETTING_ALWAYS) {
1468              // We create a line item regardless if the definition contains one or not.
1469              $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1470              $config->grade_modgrade_point = 100;
1471          }
1472          if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
1473              if (isset($item->lineItem)) {
1474                  $lineitem = $item->lineItem;
1475                  $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1476                  $maxscore = 100;
1477                  if (isset($lineitem->scoreConstraints)) {
1478                      $sc = $lineitem->scoreConstraints;
1479                      if (isset($sc->totalMaximum)) {
1480                          $maxscore = $sc->totalMaximum;
1481                      } else if (isset($sc->normalMaximum)) {
1482                          $maxscore = $sc->normalMaximum;
1483                      }
1484                  }
1485                  $config->grade_modgrade_point = $maxscore;
1486                  $config->lineitemresourceid = '';
1487                  $config->lineitemtag = '';
1488                  $config->lineitemsubreviewurl = '';
1489                  $config->lineitemsubreviewparams = '';
1490                  if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
1491                      $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';
1492                  }
1493                  if (isset($lineitem->tag)) {
1494                      $config->lineitemtag = $lineitem->tag?:'';
1495                  }
1496                  if (isset($lineitem->submissionReview)) {
1497                      $subreview = $lineitem->submissionReview;
1498                      $config->lineitemsubreviewurl = 'DEFAULT';
1499                      if (!empty($subreview->url)) {
1500                          $config->lineitemsubreviewurl = $subreview->url;
1501                      }
1502                      if (isset($subreview->custom)) {
1503                          $config->lineitemsubreviewparams = params_to_string($subreview->custom);
1504                      }
1505                  }
1506              }
1507          }
1508      }
1509      $config->instructorchoicesendname = LTI_SETTING_NEVER;
1510      $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
1511  
1512      // Since 4.3, the launch container is dictated by the value set in tool configuration and isn't controllable by content items.
1513      $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
1514  
1515      if (isset($item->custom)) {
1516          $config->instructorcustomparameters = params_to_string($item->custom);
1517      }
1518  
1519      // Set the status, allowing the form to validate, and pass an indicator to the relevant form field.
1520      $config->selectcontentstatus = true;
1521      $config->selectcontentindicator = $OUTPUT->pix_icon('i/valid', get_string('yes')) . get_string('contentselected', 'mod_lti');
1522  
1523      return $config;
1524  }
1525  
1526  /**
1527   * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
1528   * selected content item. This configuration data can be then used when adding a tool into the course.
1529   *
1530   * @param int $typeid The tool type ID.
1531   * @param string $messagetype The value for the lti_message_type parameter.
1532   * @param string $ltiversion The value for the lti_version parameter.
1533   * @param string $consumerkey The consumer key.
1534   * @param string $contentitemsjson The JSON string for the content_items parameter.
1535   * @return stdClass The array of module information objects.
1536   * @throws moodle_exception
1537   * @throws lti\OAuthException
1538   */
1539  function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
1540      $tool = lti_get_type($typeid);
1541      // Validate parameters.
1542      if (!$tool) {
1543          throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1544      }
1545      // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
1546      // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
1547      if ($messagetype !== 'ContentItemSelection') {
1548          debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
1549              DEBUG_DEVELOPER);
1550      }
1551  
1552      // Check LTI versions from our side and the response's side. Show debugging if they don't match.
1553      // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
1554      $expectedversion = $tool->ltiversion;
1555      $islti2 = ($expectedversion === LTI_VERSION_2);
1556      if ($ltiversion !== $expectedversion) {
1557          debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
1558              " Response: {$ltiversion}", DEBUG_DEVELOPER);
1559      }
1560  
1561      $items = json_decode($contentitemsjson);
1562      if (empty($items)) {
1563          throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
1564      }
1565      if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {
1566          throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
1567      }
1568  
1569      $config = null;
1570      $items = $items->{'@graph'};
1571      if (!empty($items)) {
1572          $typeconfig = lti_get_type_type_config($tool->id);
1573          if (count($items) == 1) {
1574              $config = content_item_to_form($tool, $typeconfig, $items[0]);
1575          } else {
1576              $multiple = [];
1577              foreach ($items as $item) {
1578                  $multiple[] = content_item_to_form($tool, $typeconfig, $item);
1579              }
1580              $config = new stdClass();
1581              $config->multiple = $multiple;
1582          }
1583      }
1584      return $config;
1585  }
1586  
1587  /**
1588   * Converts the new Deep-Linking format for Content-Items to the old format.
1589   *
1590   * @param string $param JSON string representing new Deep-Linking format
1591   * @return string  JSON representation of content-items
1592   */
1593  function lti_convert_content_items($param) {
1594      $items = array();
1595      $json = json_decode($param);
1596      if (!empty($json) && is_array($json)) {
1597          foreach ($json as $item) {
1598              if (isset($item->type)) {
1599                  $newitem = clone $item;
1600                  switch ($item->type) {
1601                      case 'ltiResourceLink':
1602                          $newitem->{'@type'} = 'LtiLinkItem';
1603                          $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1604                          break;
1605                      case 'link':
1606                      case 'rich':
1607                          $newitem->{'@type'} = 'ContentItem';
1608                          $newitem->mediaType = 'text/html';
1609                          break;
1610                      case 'file':
1611                          $newitem->{'@type'} = 'FileItem';
1612                          break;
1613                  }
1614                  unset($newitem->type);
1615                  if (isset($item->html)) {
1616                      $newitem->text = $item->html;
1617                      unset($newitem->html);
1618                  }
1619                  if (isset($item->iframe)) {
1620                      // DeepLinking allows multiple options to be declared as supported.
1621                      // We favor iframe over new window if both are specified.
1622                      $newitem->placementAdvice = new stdClass();
1623                      $newitem->placementAdvice->presentationDocumentTarget = 'iframe';
1624                      if (isset($item->iframe->width)) {
1625                          $newitem->placementAdvice->displayWidth = $item->iframe->width;
1626                      }
1627                      if (isset($item->iframe->height)) {
1628                          $newitem->placementAdvice->displayHeight = $item->iframe->height;
1629                      }
1630                      unset($newitem->iframe);
1631                      unset($newitem->window);
1632                  } else if (isset($item->window)) {
1633                      $newitem->placementAdvice = new stdClass();
1634                      $newitem->placementAdvice->presentationDocumentTarget = 'window';
1635                      if (isset($item->window->targetName)) {
1636                          $newitem->placementAdvice->windowTarget = $item->window->targetName;
1637                      }
1638                      if (isset($item->window->width)) {
1639                          $newitem->placementAdvice->displayWidth = $item->window->width;
1640                      }
1641                      if (isset($item->window->height)) {
1642                          $newitem->placementAdvice->displayHeight = $item->window->height;
1643                      }
1644                      unset($newitem->window);
1645                  } else if (isset($item->presentation)) {
1646                      // This may have been part of an early draft but is not in the final spec
1647                      // so keeping it around for now in case it's actually been used.
1648                      $newitem->placementAdvice = new stdClass();
1649                      if (isset($item->presentation->documentTarget)) {
1650                          $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
1651                      }
1652                      if (isset($item->presentation->windowTarget)) {
1653                          $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget;
1654                      }
1655                      if (isset($item->presentation->width)) {
1656                          $newitem->placementAdvice->dislayWidth = $item->presentation->width;
1657                      }
1658                      if (isset($item->presentation->height)) {
1659                          $newitem->placementAdvice->dislayHeight = $item->presentation->height;
1660                      }
1661                      unset($newitem->presentation);
1662                  }
1663                  if (isset($item->icon) && isset($item->icon->url)) {
1664                      $newitem->icon->{'@id'} = $item->icon->url;
1665                      unset($newitem->icon->url);
1666                  }
1667                  if (isset($item->thumbnail) && isset($item->thumbnail->url)) {
1668                      $newitem->thumbnail->{'@id'} = $item->thumbnail->url;
1669                      unset($newitem->thumbnail->url);
1670                  }
1671                  if (isset($item->lineItem)) {
1672                      unset($newitem->lineItem);
1673                      $newitem->lineItem = new stdClass();
1674                      $newitem->lineItem->{'@type'} = 'LineItem';
1675                      $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore';
1676                      if (isset($item->lineItem->label)) {
1677                          $newitem->lineItem->label = $item->lineItem->label;
1678                      }
1679                      if (isset($item->lineItem->resourceId)) {
1680                          $newitem->lineItem->assignedActivity = new stdClass();
1681                          $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId;
1682                      }
1683                      if (isset($item->lineItem->tag)) {
1684                          $newitem->lineItem->tag = $item->lineItem->tag;
1685                      }
1686                      if (isset($item->lineItem->scoreMaximum)) {
1687                          $newitem->lineItem->scoreConstraints = new stdClass();
1688                          $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits';
1689                          $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum;
1690                      }
1691                      if (isset($item->lineItem->submissionReview)) {
1692                          $newitem->lineItem->submissionReview = $item->lineItem->submissionReview;
1693                      }
1694                  }
1695                  $items[] = $newitem;
1696              }
1697          }
1698      }
1699  
1700      $newitems = new stdClass();
1701      $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1702      $newitems->{'@graph'} = $items;
1703  
1704      return json_encode($newitems);
1705  }
1706  
1707  function lti_get_tool_table($tools, $id) {
1708      global $OUTPUT;
1709      $html = '';
1710  
1711      $typename = get_string('typename', 'lti');
1712      $baseurl = get_string('baseurl', 'lti');
1713      $action = get_string('action', 'lti');
1714      $createdon = get_string('createdon', 'lti');
1715  
1716      if (!empty($tools)) {
1717          $html .= "
1718          <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\">
1719              <table id=\"{$id}_tools\">
1720                  <thead>
1721                      <tr>
1722                          <th>$typename</th>
1723                          <th>$baseurl</th>
1724                          <th>$createdon</th>
1725                          <th>$action</th>
1726                      </tr>
1727                  </thead>
1728          ";
1729  
1730          foreach ($tools as $type) {
1731              $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1732              $accept = get_string('accept', 'lti');
1733              $update = get_string('update', 'lti');
1734              $delete = get_string('delete', 'lti');
1735  
1736              if (empty($type->toolproxyid)) {
1737                  $baseurl = new \moodle_url('/mod/lti/typessettings.php', array(
1738                          'action' => 'accept',
1739                          'id' => $type->id,
1740                          'sesskey' => sesskey(),
1741                          'tab' => $id
1742                      ));
1743                  $ref = $type->baseurl;
1744              } else {
1745                  $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array(
1746                          'action' => 'accept',
1747                          'id' => $type->id,
1748                          'sesskey' => sesskey(),
1749                          'tab' => $id
1750                      ));
1751                  $ref = $type->tpname;
1752              }
1753  
1754              $accepthtml = $OUTPUT->action_icon($baseurl,
1755                      new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1756                      array('title' => $accept, 'class' => 'editing_accept'));
1757  
1758              $deleteaction = 'delete';
1759  
1760              if ($type->state == LTI_TOOL_STATE_CONFIGURED) {
1761                  $accepthtml = '';
1762              }
1763  
1764              if ($type->state != LTI_TOOL_STATE_REJECTED) {
1765                  $deleteaction = 'reject';
1766                  $delete = get_string('reject', 'lti');
1767              }
1768  
1769              $updateurl = clone($baseurl);
1770              $updateurl->param('action', 'update');
1771              $updatehtml = $OUTPUT->action_icon($updateurl,
1772                      new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1773                      array('title' => $update, 'class' => 'editing_update'));
1774  
1775              if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) {
1776                  $deleteurl = clone($baseurl);
1777                  $deleteurl->param('action', $deleteaction);
1778                  $deletehtml = $OUTPUT->action_icon($deleteurl,
1779                          new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1780                          array('title' => $delete, 'class' => 'editing_delete'));
1781              } else {
1782                  $deletehtml = '';
1783              }
1784              $html .= "
1785              <tr>
1786                  <td>
1787                      {$type->name}
1788                  </td>
1789                  <td>
1790                      {$ref}
1791                  </td>
1792                  <td>
1793                      {$date}
1794                  </td>
1795                  <td align=\"center\">
1796                      {$accepthtml}{$updatehtml}{$deletehtml}
1797                  </td>
1798              </tr>
1799              ";
1800          }
1801          $html .= '</table></div>';
1802      } else {
1803          $html .= get_string('no_' . $id, 'lti');
1804      }
1805  
1806      return $html;
1807  }
1808  
1809  /**
1810   * This function builds the tab for a category of tool proxies
1811   *
1812   * @param object    $toolproxies    Tool proxy instance objects
1813   * @param string    $id             Category ID
1814   *
1815   * @return string                   HTML for tab
1816   */
1817  function lti_get_tool_proxy_table($toolproxies, $id) {
1818      global $OUTPUT;
1819  
1820      if (!empty($toolproxies)) {
1821          $typename = get_string('typename', 'lti');
1822          $url = get_string('registrationurl', 'lti');
1823          $action = get_string('action', 'lti');
1824          $createdon = get_string('createdon', 'lti');
1825  
1826          $html = <<< EOD
1827          <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em">
1828              <table id="{$id}_tool_proxies">
1829                  <thead>
1830                      <tr>
1831                          <th>{$typename}</th>
1832                          <th>{$url}</th>
1833                          <th>{$createdon}</th>
1834                          <th>{$action}</th>
1835                      </tr>
1836                  </thead>
1837  EOD;
1838          foreach ($toolproxies as $toolproxy) {
1839              $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1840              $accept = get_string('register', 'lti');
1841              $update = get_string('update', 'lti');
1842              $delete = get_string('delete', 'lti');
1843  
1844              $baseurl = new \moodle_url('/mod/lti/registersettings.php', array(
1845                      'action' => 'accept',
1846                      'id' => $toolproxy->id,
1847                      'sesskey' => sesskey(),
1848                      'tab' => $id
1849                  ));
1850  
1851              $registerurl = new \moodle_url('/mod/lti/register.php', array(
1852                      'id' => $toolproxy->id,
1853                      'sesskey' => sesskey(),
1854                      'tab' => 'tool_proxy'
1855                  ));
1856  
1857              $accepthtml = $OUTPUT->action_icon($registerurl,
1858                      new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1859                      array('title' => $accept, 'class' => 'editing_accept'));
1860  
1861              $deleteaction = 'delete';
1862  
1863              if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) {
1864                  $accepthtml = '';
1865              }
1866  
1867              if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) {
1868                  $delete = get_string('cancel', 'lti');
1869              }
1870  
1871              $updateurl = clone($baseurl);
1872              $updateurl->param('action', 'update');
1873              $updatehtml = $OUTPUT->action_icon($updateurl,
1874                      new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1875                      array('title' => $update, 'class' => 'editing_update'));
1876  
1877              $deleteurl = clone($baseurl);
1878              $deleteurl->param('action', $deleteaction);
1879              $deletehtml = $OUTPUT->action_icon($deleteurl,
1880                      new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1881                      array('title' => $delete, 'class' => 'editing_delete'));
1882              $html .= <<< EOD
1883              <tr>
1884                  <td>
1885                      {$toolproxy->name}
1886                  </td>
1887                  <td>
1888                      {$toolproxy->regurl}
1889                  </td>
1890                  <td>
1891                      {$date}
1892                  </td>
1893                  <td align="center">
1894                      {$accepthtml}{$updatehtml}{$deletehtml}
1895                  </td>
1896              </tr>
1897  EOD;
1898          }
1899          $html .= '</table></div>';
1900      } else {
1901          $html = get_string('no_' . $id, 'lti');
1902      }
1903  
1904      return $html;
1905  }
1906  
1907  /**
1908   * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter
1909   *
1910   * @param object $tool  Tool instance object
1911   *
1912   * @return array List of enabled capabilities
1913   */
1914  function lti_get_enabled_capabilities($tool) {
1915      if (!isset($tool)) {
1916          return array();
1917      }
1918      if (!empty($tool->enabledcapability)) {
1919          $enabledcapabilities = explode("\n", $tool->enabledcapability);
1920      } else {
1921          $enabledcapabilities = array();
1922      }
1923      if (!empty($tool->parameter)) {
1924          $paramstr = str_replace("\r\n", "\n", $tool->parameter);
1925          $paramstr = str_replace("\n\r", "\n", $paramstr);
1926          $paramstr = str_replace("\r", "\n", $paramstr);
1927          $params = explode("\n", $paramstr);
1928          foreach ($params as $param) {
1929              $pos = strpos($param, '=');
1930              if (($pos === false) || ($pos < 1)) {
1931                  continue;
1932              }
1933              $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
1934              if (substr($value, 0, 1) == '$') {
1935                  $value = substr($value, 1);
1936                  if (!in_array($value, $enabledcapabilities)) {
1937                      $enabledcapabilities[] = $value;
1938                  }
1939              }
1940          }
1941      }
1942      return $enabledcapabilities;
1943  }
1944  
1945  /**
1946   * Splits the custom parameters
1947   *
1948   * @param string    $customstr      String containing the parameters
1949   *
1950   * @return array of custom parameters
1951   */
1952  function lti_split_parameters($customstr) {
1953      $customstr = str_replace("\r\n", "\n", $customstr);
1954      $customstr = str_replace("\n\r", "\n", $customstr);
1955      $customstr = str_replace("\r", "\n", $customstr);
1956      $lines = explode("\n", $customstr);  // Or should this split on "/[\n;]/"?
1957      $retval = array();
1958      foreach ($lines as $line) {
1959          $pos = strpos($line, '=');
1960          if ( $pos === false || $pos < 1 ) {
1961              continue;
1962          }
1963          $key = trim(core_text::substr($line, 0, $pos));
1964          $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
1965          $retval[$key] = $val;
1966      }
1967      return $retval;
1968  }
1969  
1970  /**
1971   * Splits the custom parameters field to the various parameters
1972   *
1973   * @param object    $toolproxy      Tool proxy instance object
1974   * @param object    $tool           Tool instance object
1975   * @param array     $params         LTI launch parameters
1976   * @param string    $customstr      String containing the parameters
1977   * @param boolean   $islti2         True if an LTI 2 tool is being launched
1978   *
1979   * @return array of custom parameters
1980   */
1981  function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {
1982      $splitted = lti_split_parameters($customstr);
1983      $retval = array();
1984      foreach ($splitted as $key => $val) {
1985          $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
1986          $key2 = lti_map_keyname($key);
1987          $retval['custom_'.$key2] = $val;
1988          if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) {
1989              $retval['custom_'.$key] = $val;
1990          }
1991      }
1992      return $retval;
1993  }
1994  
1995  /**
1996   * Adds the custom parameters to an array
1997   *
1998   * @param object    $toolproxy      Tool proxy instance object
1999   * @param object    $tool           Tool instance object
2000   * @param array     $params         LTI launch parameters
2001   * @param array     $parameters     Array containing the parameters
2002   *
2003   * @return array    Array of custom parameters
2004   */
2005  function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {
2006      $retval = array();
2007      foreach ($parameters as $key => $val) {
2008          $key2 = lti_map_keyname($key);
2009          $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true);
2010          $retval['custom_'.$key2] = $val;
2011          if ($key != $key2) {
2012              $retval['custom_'.$key] = $val;
2013          }
2014      }
2015      return $retval;
2016  }
2017  
2018  /**
2019   * Parse a custom parameter to replace any substitution variables
2020   *
2021   * @param object    $toolproxy      Tool proxy instance object
2022   * @param object    $tool           Tool instance object
2023   * @param array     $params         LTI launch parameters
2024   * @param string    $value          Custom parameter value
2025   * @param boolean   $islti2         True if an LTI 2 tool is being launched
2026   *
2027   * @return string Parsed value of custom parameter
2028   */
2029  function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {
2030      // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var.
2031      global $USER, $COURSE;
2032  
2033      if ($value) {
2034          if (substr($value, 0, 1) == '\\') {
2035              $value = substr($value, 1);
2036          } else if (substr($value, 0, 1) == '$') {
2037              $value1 = substr($value, 1);
2038              $enabledcapabilities = lti_get_enabled_capabilities($tool);
2039              if (!$islti2 || in_array($value1, $enabledcapabilities)) {
2040                  $capabilities = lti_get_capabilities();
2041                  if (array_key_exists($value1, $capabilities)) {
2042                      $val = $capabilities[$value1];
2043                      if ($val) {
2044                          if (substr($val, 0, 1) != '$') {
2045                              $value = $params[$val];
2046                          } else {
2047                              $valarr = explode('->', substr($val, 1), 2);
2048                              $value = "{${$valarr[0]}->{$valarr[1]}}";
2049                              $value = str_replace('<br />' , ' ', $value);
2050                              $value = str_replace('<br>' , ' ', $value);
2051                              $value = format_string($value);
2052                          }
2053                      } else {
2054                          $value = lti_calculate_custom_parameter($value1);
2055                      }
2056                  } else {
2057                      $val = $value;
2058                      $services = lti_get_services();
2059                      foreach ($services as $service) {
2060                          $service->set_tool_proxy($toolproxy);
2061                          $service->set_type($tool);
2062                          $value = $service->parse_value($val);
2063                          if ($val != $value) {
2064                              break;
2065                          }
2066                      }
2067                  }
2068              }
2069          }
2070      }
2071      return $value;
2072  }
2073  
2074  /**
2075   * Calculates the value of a custom parameter that has not been specified earlier
2076   *
2077   * @param string    $value          Custom parameter value
2078   *
2079   * @return string Calculated value of custom parameter
2080   */
2081  function lti_calculate_custom_parameter($value) {
2082      global $USER, $COURSE;
2083  
2084      switch ($value) {
2085          case 'Moodle.Person.userGroupIds':
2086              return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]);
2087          case 'Context.id.history':
2088              return implode(",", get_course_history($COURSE));
2089          case 'CourseSection.timeFrame.begin':
2090              if (empty($COURSE->startdate)) {
2091                  return "";
2092              }
2093              $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC'));
2094              return $dt->format(DateTime::ATOM);
2095          case 'CourseSection.timeFrame.end':
2096              if (empty($COURSE->enddate)) {
2097                  return "";
2098              }
2099              $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC'));
2100              return $dt->format(DateTime::ATOM);
2101      }
2102      return null;
2103  }
2104  
2105  /**
2106   * Build the history chain for this course using the course originalcourseid.
2107   *
2108   * @param object $course course for which the history is returned.
2109   *
2110   * @return array ids of the source course in ancestry order, immediate parent 1st.
2111   */
2112  function get_course_history($course) {
2113      global $DB;
2114      $history = [];
2115      $parentid = $course->originalcourseid;
2116      while (!empty($parentid) && !in_array($parentid, $history)) {
2117          $history[] = $parentid;
2118          $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid));
2119      }
2120      return $history;
2121  }
2122  
2123  /**
2124   * Used for building the names of the different custom parameters
2125   *
2126   * @param string $key   Parameter name
2127   * @param bool $tolower Do we want to convert the key into lower case?
2128   * @return string       Processed name
2129   */
2130  function lti_map_keyname($key, $tolower = true) {
2131      if ($tolower) {
2132          $newkey = '';
2133          $key = core_text::strtolower(trim($key));
2134          foreach (str_split($key) as $ch) {
2135              if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {
2136                  $newkey .= $ch;
2137              } else {
2138                  $newkey .= '_';
2139              }
2140          }
2141      } else {
2142          $newkey = $key;
2143      }
2144      return $newkey;
2145  }
2146  
2147  /**
2148   * Gets the IMS role string for the specified user and LTI course module.
2149   *
2150   * @param mixed    $user      User object or user id
2151   * @param int      $cmid      The course module id of the LTI activity
2152   * @param int      $courseid  The course id of the LTI activity
2153   * @param boolean  $islti2    True if an LTI 2 tool is being launched
2154   *
2155   * @return string A role string suitable for passing with an LTI launch
2156   */
2157  function lti_get_ims_role($user, $cmid, $courseid, $islti2) {
2158      $roles = array();
2159  
2160      if (empty($cmid)) {
2161          // If no cmid is passed, check if the user is a teacher in the course
2162          // This allows other modules to programmatically "fake" a launch without
2163          // a real LTI instance.
2164          $context = context_course::instance($courseid);
2165  
2166          if (has_capability('moodle/course:manageactivities', $context, $user)) {
2167              array_push($roles, 'Instructor');
2168          } else {
2169              array_push($roles, 'Learner');
2170          }
2171      } else {
2172          $context = context_module::instance($cmid);
2173  
2174          if (has_capability('mod/lti:manage', $context)) {
2175              array_push($roles, 'Instructor');
2176          } else {
2177              array_push($roles, 'Learner');
2178          }
2179      }
2180  
2181      if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) {
2182          // Make sure admins do not have the Learner role, then set admin role.
2183          $roles = array_diff($roles, array('Learner'));
2184          if (!$islti2) {
2185              array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
2186          } else {
2187              array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator');
2188          }
2189      }
2190  
2191      return join(',', $roles);
2192  }
2193  
2194  /**
2195   * Returns configuration details for the tool
2196   *
2197   * @param int $typeid   Basic LTI tool typeid
2198   *
2199   * @return array        Tool Configuration
2200   */
2201  function lti_get_type_config($typeid) {
2202      global $DB;
2203  
2204      $query = "SELECT name, value
2205                  FROM {lti_types_config}
2206                 WHERE typeid = :typeid1
2207             UNION ALL
2208                SELECT 'toolurl' AS name, baseurl AS value
2209                  FROM {lti_types}
2210                 WHERE id = :typeid2
2211             UNION ALL
2212                SELECT 'icon' AS name, icon AS value
2213                  FROM {lti_types}
2214                 WHERE id = :typeid3
2215             UNION ALL
2216                SELECT 'secureicon' AS name, secureicon AS value
2217                  FROM {lti_types}
2218                 WHERE id = :typeid4";
2219  
2220      $typeconfig = array();
2221      $configs = $DB->get_records_sql($query,
2222          array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid));
2223  
2224      if (!empty($configs)) {
2225          foreach ($configs as $config) {
2226              $typeconfig[$config->name] = $config->value;
2227          }
2228      }
2229  
2230      return $typeconfig;
2231  }
2232  
2233  function lti_get_tools_by_url($url, $state, $courseid = null) {
2234      $domain = lti_get_domain_from_url($url);
2235  
2236      return lti_get_tools_by_domain($domain, $state, $courseid);
2237  }
2238  
2239  function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
2240      global $DB, $SITE;
2241  
2242      $statefilter = '';
2243      $coursefilter = '';
2244  
2245      if ($state) {
2246          $statefilter = 'AND t.state = :state';
2247      }
2248  
2249      if ($courseid && $courseid != $SITE->id) {
2250          $coursefilter = 'OR t.course = :courseid';
2251      }
2252  
2253      $coursecategory = $DB->get_field('course', 'category', ['id' => $courseid]);
2254      $query = "SELECT t.*
2255                  FROM {lti_types} t
2256             LEFT JOIN {lti_types_categories} tc on t.id = tc.typeid
2257                 WHERE t.tooldomain = :tooldomain
2258                   AND (t.course = :siteid $coursefilter)
2259                   $statefilter
2260                   AND (tc.id IS NULL OR tc.categoryid = :categoryid)";
2261  
2262      return $DB->get_records_sql($query, [
2263              'courseid' => $courseid,
2264              'siteid' => $SITE->id,
2265              'tooldomain' => $domain,
2266              'state' => $state,
2267              'categoryid' => $coursecategory
2268          ]);
2269  }
2270  
2271  /**
2272   * Returns all basicLTI tools configured by the administrator
2273   *
2274   * @param int $course
2275   *
2276   * @return array
2277   */
2278  function lti_filter_get_types($course) {
2279      global $DB;
2280  
2281      if (!empty($course)) {
2282          $where = "WHERE t.course = :course";
2283          $params = array('course' => $course);
2284      } else {
2285          $where = '';
2286          $params = array();
2287      }
2288      $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname
2289                  FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id
2290                  {$where}";
2291      return $DB->get_records_sql($query, $params);
2292  }
2293  
2294  /**
2295   * Given an array of tools, filter them based on their state
2296   *
2297   * @param array $tools An array of lti_types records
2298   * @param int $state One of the LTI_TOOL_STATE_* constants
2299   * @return array
2300   */
2301  function lti_filter_tool_types(array $tools, $state) {
2302      $return = array();
2303      foreach ($tools as $key => $tool) {
2304          if ($tool->state == $state) {
2305              $return[$key] = $tool;
2306          }
2307      }
2308      return $return;
2309  }
2310  
2311  /**
2312   * Returns all lti types visible in this course
2313   *
2314   * @deprecated since Moodle 4.3
2315   * @param int $courseid The id of the course to retieve types for
2316   * @param array $coursevisible options for 'coursevisible' field,
2317   *        default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]
2318   * @return stdClass[] All the lti types visible in the given course
2319   */
2320  function lti_get_lti_types_by_course($courseid, $coursevisible = null) {
2321      debugging(__FUNCTION__ . '() is deprecated. Please use \mod_lti\local\types_helper::get_lti_types_by_course() instead.',
2322          DEBUG_DEVELOPER);
2323  
2324      global $USER;
2325      return \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id, $coursevisible ?? []);
2326  }
2327  
2328  /**
2329   * Returns tool types for lti add instance and edit page
2330   *
2331   * @return array Array of lti types
2332   */
2333  function lti_get_types_for_add_instance() {
2334      global $COURSE, $USER;
2335  
2336      // Always return the 'manual' type option, despite manual config being deprecated, so that we have it for legacy instances.
2337      $types = [(object) ['name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null]];
2338  
2339      $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($COURSE->id, $USER->id);
2340      foreach ($preconfiguredtypes as $type) {
2341          $types[$type->id] = $type;
2342      }
2343  
2344      return $types;
2345  }
2346  
2347  /**
2348   * Returns a list of configured types in the given course
2349   *
2350   * @param int $courseid The id of the course to retieve types for
2351   * @param int $sectionreturn section to return to for forming the URLs
2352   * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link
2353   */
2354  function lti_get_configured_types($courseid, $sectionreturn = 0) {
2355      global $OUTPUT, $USER;
2356      $types = [];
2357      $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id,
2358          [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]);
2359  
2360      foreach ($preconfiguredtypes as $ltitype) {
2361          $type           = new stdClass();
2362          $type->id       = $ltitype->id;
2363          $type->modclass = MOD_CLASS_ACTIVITY;
2364          $type->name     = 'lti_type_' . $ltitype->id;
2365          // Clean the name. We don't want tags here.
2366          $type->title    = clean_param($ltitype->name, PARAM_NOTAGS);
2367          $trimmeddescription = trim($ltitype->description ?? '');
2368          if ($trimmeddescription != '') {
2369              // Clean the description. We don't want tags here.
2370              $type->help     = clean_param($trimmeddescription, PARAM_NOTAGS);
2371              $type->helplink = get_string('modulename_shortcut_link', 'lti');
2372          }
2373  
2374          $iconurl = get_tool_type_icon_url($ltitype);
2375          $iconclass = '';
2376          if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) {
2377              // Do not filter the icon if it is not the default LTI activity icon.
2378              $iconclass = 'nofilter';
2379          }
2380          $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]);
2381  
2382          $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'course' => $courseid,
2383              'sr' => $sectionreturn, 'typeid' => $ltitype->id));
2384          $types[] = $type;
2385      }
2386      return $types;
2387  }
2388  
2389  function lti_get_domain_from_url($url) {
2390      $matches = array();
2391  
2392      if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) {
2393          return $matches[1];
2394      }
2395  }
2396  
2397  function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) {
2398      $possibletools = lti_get_tools_by_url($url, $state, $courseid);
2399  
2400      return lti_get_best_tool_by_url($url, $possibletools, $courseid);
2401  }
2402  
2403  function lti_get_url_thumbprint($url) {
2404      // Parse URL requires a schema otherwise everything goes into 'path'.  Fixed 5.4.7 or later.
2405      if (preg_match('/https?:\/\//', $url) !== 1) {
2406          $url = 'http://'.$url;
2407      }
2408      $urlparts = parse_url(strtolower($url));
2409      if (!isset($urlparts['path'])) {
2410          $urlparts['path'] = '';
2411      }
2412  
2413      if (!isset($urlparts['query'])) {
2414          $urlparts['query'] = '';
2415      }
2416  
2417      if (!isset($urlparts['host'])) {
2418          $urlparts['host'] = '';
2419      }
2420  
2421      if (substr($urlparts['host'], 0, 4) === 'www.') {
2422          $urlparts['host'] = substr($urlparts['host'], 4);
2423      }
2424  
2425      $urllower = $urlparts['host'] . '/' . $urlparts['path'];
2426  
2427      if ($urlparts['query'] != '') {
2428          $urllower .= '?' . $urlparts['query'];
2429      }
2430  
2431      return $urllower;
2432  }
2433  
2434  function lti_get_best_tool_by_url($url, $tools, $courseid = null) {
2435      if (count($tools) === 0) {
2436          return null;
2437      }
2438  
2439      $urllower = lti_get_url_thumbprint($url);
2440  
2441      foreach ($tools as $tool) {
2442          $tool->_matchscore = 0;
2443  
2444          $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl);
2445  
2446          if ($urllower === $toolbaseurllower) {
2447              // 100 points for exact thumbprint match.
2448              $tool->_matchscore += 100;
2449          } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) {
2450              // 50 points if tool thumbprint starts with the base URL thumbprint.
2451              $tool->_matchscore += 50;
2452          }
2453  
2454          // Prefer course tools over site tools.
2455          if (!empty($courseid)) {
2456              // Minus 10 points for not matching the course id (global tools).
2457              if ($tool->course != $courseid) {
2458                  $tool->_matchscore -= 10;
2459              }
2460          }
2461      }
2462  
2463      $bestmatch = array_reduce($tools, function($value, $tool) {
2464          if ($tool->_matchscore > $value->_matchscore) {
2465              return $tool;
2466          } else {
2467              return $value;
2468          }
2469  
2470      }, (object)array('_matchscore' => -1));
2471  
2472      // None of the tools are suitable for this URL.
2473      if ($bestmatch->_matchscore <= 0) {
2474          return null;
2475      }
2476  
2477      return $bestmatch;
2478  }
2479  
2480  function lti_get_shared_secrets_by_key($key) {
2481      global $DB;
2482  
2483      // Look up the shared secret for the specified key in both the types_config table (for configured tools)
2484      // And in the lti resource table for ad-hoc tools.
2485      $lti13 = LTI_VERSION_1P3;
2486      $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value
2487                  FROM {lti_types_config} t1
2488                  JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
2489                  JOIN {lti_types} type ON t2.typeid = type.id
2490                WHERE t1.name = 'resourcekey'
2491                  AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1
2492                  AND t2.name = 'password'
2493                  AND type.state = :configured1
2494                  AND type.ltiversion <> :ltiversion
2495                 UNION
2496                SELECT tp.secret AS value
2497                  FROM {lti_tool_proxies} tp
2498                  JOIN {lti_types} t ON tp.id = t.toolproxyid
2499                WHERE tp.guid = :key2
2500                  AND t.state = :configured2
2501                 UNION
2502                SELECT password AS value
2503                 FROM {lti}
2504                WHERE resourcekey = :key3";
2505  
2506      $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13,
2507          'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));
2508  
2509      $values = array_map(function($item) {
2510          return $item->value;
2511      }, $sharedsecrets);
2512  
2513      // There should really only be one shared secret per key. But, we can't prevent
2514      // more than one getting entered. For instance, if the same key is used for two tool providers.
2515      return $values;
2516  }
2517  
2518  /**
2519   * Delete a Basic LTI configuration
2520   *
2521   * @param int $id   Configuration id
2522   */
2523  function lti_delete_type($id) {
2524      global $DB;
2525  
2526      // We should probably just copy the launch URL to the tool instances in this case... using a single query.
2527      /*
2528      $instances = $DB->get_records('lti', array('typeid' => $id));
2529      foreach ($instances as $instance) {
2530          $instance->typeid = 0;
2531          $DB->update_record('lti', $instance);
2532      }*/
2533  
2534      $DB->delete_records('lti_types', array('id' => $id));
2535      $DB->delete_records('lti_types_config', array('typeid' => $id));
2536      $DB->delete_records('lti_types_categories', array('typeid' => $id));
2537  }
2538  
2539  function lti_set_state_for_type($id, $state) {
2540      global $DB;
2541  
2542      $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));
2543  }
2544  
2545  /**
2546   * Transforms a basic LTI object to an array
2547   *
2548   * @param object $ltiobject    Basic LTI object
2549   *
2550   * @return array Basic LTI configuration details
2551   */
2552  function lti_get_config($ltiobject) {
2553      $typeconfig = (array)$ltiobject;
2554      $additionalconfig = lti_get_type_config($ltiobject->typeid);
2555      $typeconfig = array_merge($typeconfig, $additionalconfig);
2556      return $typeconfig;
2557  }
2558  
2559  /**
2560   *
2561   * Generates some of the tool configuration based on the instance details
2562   *
2563   * @param int $id
2564   *
2565   * @return object configuration
2566   *
2567   */
2568  function lti_get_type_config_from_instance($id) {
2569      global $DB;
2570  
2571      $instance = $DB->get_record('lti', array('id' => $id));
2572      $config = lti_get_config($instance);
2573  
2574      $type = new \stdClass();
2575      $type->lti_fix = $id;
2576      if (isset($config['toolurl'])) {
2577          $type->lti_toolurl = $config['toolurl'];
2578      }
2579      if (isset($config['instructorchoicesendname'])) {
2580          $type->lti_sendname = $config['instructorchoicesendname'];
2581      }
2582      if (isset($config['instructorchoicesendemailaddr'])) {
2583          $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr'];
2584      }
2585      if (isset($config['instructorchoiceacceptgrades'])) {
2586          $type->lti_acceptgrades = $config['instructorchoiceacceptgrades'];
2587      }
2588      if (isset($config['instructorchoiceallowroster'])) {
2589          $type->lti_allowroster = $config['instructorchoiceallowroster'];
2590      }
2591  
2592      if (isset($config['instructorcustomparameters'])) {
2593          $type->lti_allowsetting = $config['instructorcustomparameters'];
2594      }
2595      return $type;
2596  }
2597  
2598  /**
2599   * Generates some of the tool configuration based on the admin configuration details
2600   *
2601   * @param int $id
2602   *
2603   * @return stdClass Configuration details
2604   */
2605  function lti_get_type_type_config($id) {
2606      global $DB;
2607  
2608      $basicltitype = $DB->get_record('lti_types', array('id' => $id));
2609      $config = lti_get_type_config($id);
2610  
2611      $type = new \stdClass();
2612  
2613      $type->lti_typename = $basicltitype->name;
2614  
2615      $type->typeid = $basicltitype->id;
2616  
2617      $type->course = $basicltitype->course;
2618  
2619      $type->toolproxyid = $basicltitype->toolproxyid;
2620  
2621      $type->lti_toolurl = $basicltitype->baseurl;
2622  
2623      $type->lti_ltiversion = $basicltitype->ltiversion;
2624  
2625      $type->lti_clientid = $basicltitype->clientid;
2626      $type->lti_clientid_disabled = $type->lti_clientid;
2627  
2628      $type->lti_description = $basicltitype->description;
2629  
2630      $type->lti_parameters = $basicltitype->parameter;
2631  
2632      $type->lti_icon = $basicltitype->icon;
2633  
2634      $type->lti_secureicon = $basicltitype->secureicon;
2635  
2636      if (isset($config['resourcekey'])) {
2637          $type->lti_resourcekey = $config['resourcekey'];
2638      }
2639      if (isset($config['password'])) {
2640          $type->lti_password = $config['password'];
2641      }
2642      if (isset($config['publickey'])) {
2643          $type->lti_publickey = $config['publickey'];
2644      }
2645      if (isset($config['publickeyset'])) {
2646          $type->lti_publickeyset = $config['publickeyset'];
2647      }
2648      if (isset($config['keytype'])) {
2649          $type->lti_keytype = $config['keytype'];
2650      }
2651      if (isset($config['initiatelogin'])) {
2652          $type->lti_initiatelogin = $config['initiatelogin'];
2653      }
2654      if (isset($config['redirectionuris'])) {
2655          $type->lti_redirectionuris = $config['redirectionuris'];
2656      }
2657  
2658      if (isset($config['sendname'])) {
2659          $type->lti_sendname = $config['sendname'];
2660      }
2661      if (isset($config['instructorchoicesendname'])) {
2662          $type->lti_instructorchoicesendname = $config['instructorchoicesendname'];
2663      }
2664      if (isset($config['sendemailaddr'])) {
2665          $type->lti_sendemailaddr = $config['sendemailaddr'];
2666      }
2667      if (isset($config['instructorchoicesendemailaddr'])) {
2668          $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr'];
2669      }
2670      if (isset($config['acceptgrades'])) {
2671          $type->lti_acceptgrades = $config['acceptgrades'];
2672      }
2673      if (isset($config['instructorchoiceacceptgrades'])) {
2674          $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades'];
2675      }
2676      if (isset($config['allowroster'])) {
2677          $type->lti_allowroster = $config['allowroster'];
2678      }
2679      if (isset($config['instructorchoiceallowroster'])) {
2680          $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster'];
2681      }
2682  
2683      if (isset($config['customparameters'])) {
2684          $type->lti_customparameters = $config['customparameters'];
2685      }
2686  
2687      if (isset($config['forcessl'])) {
2688          $type->lti_forcessl = $config['forcessl'];
2689      }
2690  
2691      if (isset($config['organizationid_default'])) {
2692          $type->lti_organizationid_default = $config['organizationid_default'];
2693      } else {
2694          // Tool was configured before this option was available and the default then was host.
2695          $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
2696      }
2697      if (isset($config['organizationid'])) {
2698          $type->lti_organizationid = $config['organizationid'];
2699      }
2700      if (isset($config['organizationurl'])) {
2701          $type->lti_organizationurl = $config['organizationurl'];
2702      }
2703      if (isset($config['organizationdescr'])) {
2704          $type->lti_organizationdescr = $config['organizationdescr'];
2705      }
2706      if (isset($config['launchcontainer'])) {
2707          $type->lti_launchcontainer = $config['launchcontainer'];
2708      }
2709  
2710      if (isset($config['coursevisible'])) {
2711          $type->lti_coursevisible = $config['coursevisible'];
2712      }
2713  
2714      if (isset($config['contentitem'])) {
2715          $type->lti_contentitem = $config['contentitem'];
2716      }
2717  
2718      if (isset($config['toolurl_ContentItemSelectionRequest'])) {
2719          $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];
2720      }
2721  
2722      if (isset($config['debuglaunch'])) {
2723          $type->lti_debuglaunch = $config['debuglaunch'];
2724      }
2725  
2726      if (isset($config['module_class_type'])) {
2727          $type->lti_module_class_type = $config['module_class_type'];
2728      }
2729  
2730      // Get the parameters from the LTI services.
2731      foreach ($config as $name => $value) {
2732          if (strpos($name, 'ltiservice_') === 0) {
2733              $type->{$name} = $config[$name];
2734          }
2735      }
2736  
2737      return $type;
2738  }
2739  
2740  function lti_prepare_type_for_save($type, $config) {
2741      if (isset($config->lti_toolurl)) {
2742          $type->baseurl = $config->lti_toolurl;
2743          if (isset($config->lti_tooldomain)) {
2744              $type->tooldomain = $config->lti_tooldomain;
2745          } else {
2746              $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
2747          }
2748      }
2749      if (isset($config->lti_description)) {
2750          $type->description = $config->lti_description;
2751      }
2752      if (isset($config->lti_typename)) {
2753          $type->name = $config->lti_typename;
2754      }
2755      if (isset($config->lti_ltiversion)) {
2756          $type->ltiversion = $config->lti_ltiversion;
2757      }
2758      if (isset($config->lti_clientid)) {
2759          $type->clientid = $config->lti_clientid;
2760      }
2761      if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {
2762          $type->clientid = registration_helper::get()->new_clientid();
2763      } else if (empty($type->clientid)) {
2764          $type->clientid = null;
2765      }
2766      if (isset($config->lti_coursevisible)) {
2767          $type->coursevisible = $config->lti_coursevisible;
2768      }
2769  
2770      if (isset($config->lti_icon)) {
2771          $type->icon = $config->lti_icon;
2772      }
2773      if (isset($config->lti_secureicon)) {
2774          $type->secureicon = $config->lti_secureicon;
2775      }
2776  
2777      $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0;
2778      $config->lti_forcessl = $type->forcessl;
2779      if (isset($config->lti_contentitem)) {
2780          $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;
2781          $config->lti_contentitem = $type->contentitem;
2782      }
2783      if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
2784          if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {
2785              $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;
2786          } else {
2787              $type->toolurl_ContentItemSelectionRequest = '';
2788          }
2789          $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;
2790      }
2791  
2792      $type->timemodified = time();
2793  
2794      unset ($config->lti_typename);
2795      unset ($config->lti_toolurl);
2796      unset ($config->lti_description);
2797      unset ($config->lti_ltiversion);
2798      unset ($config->lti_clientid);
2799      unset ($config->lti_icon);
2800      unset ($config->lti_secureicon);
2801  }
2802  
2803  function lti_update_type($type, $config) {
2804      global $DB, $CFG;
2805  
2806      lti_prepare_type_for_save($type, $config);
2807  
2808      if (lti_request_is_using_ssl() && !empty($type->secureicon)) {
2809          $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);
2810      } else {
2811          $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon));
2812      }
2813      unset($config->oldicon);
2814  
2815      if ($DB->update_record('lti_types', $type)) {
2816          foreach ($config as $key => $value) {
2817              if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {
2818                  $record = new \StdClass();
2819                  $record->typeid = $type->id;
2820                  $record->name = substr($key, 4);
2821                  $record->value = $value;
2822                  lti_update_config($record);
2823              }
2824              if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
2825                  $record = new \StdClass();
2826                  $record->typeid = $type->id;
2827                  $record->name = $key;
2828                  $record->value = $value;
2829                  lti_update_config($record);
2830              }
2831          }
2832          if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) {
2833              // We need to remove the tool proxy for this tool to function under 1.3.
2834              $toolproxyid = $type->toolproxyid;
2835              $DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid));
2836              $DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid));
2837              $type->toolproxyid = null;
2838              $DB->update_record('lti_types', $type);
2839          }
2840          $DB->delete_records('lti_types_categories', ['typeid' => $type->id]);
2841          if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) {
2842              lti_type_add_categories($type->id, $config->lti_coursecategories);
2843          }
2844          require_once($CFG->libdir.'/modinfolib.php');
2845          if ($clearcache) {
2846              $sql = "SELECT cm.id, cm.course
2847                        FROM {course_modules} cm
2848                        JOIN {modules} m ON cm.module = m.id
2849                        JOIN {lti} l ON l.course = cm.course
2850                       WHERE m.name = :name AND l.typeid = :typeid";
2851  
2852              $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]);
2853  
2854              $courseids = [];
2855              foreach ($rs as $record) {
2856                  $courseids[] = $record->course;
2857                  \course_modinfo::purge_course_module_cache($record->course, $record->id);
2858              }
2859              $rs->close();
2860              $courseids = array_unique($courseids);
2861              foreach ($courseids as $courseid) {
2862                  rebuild_course_cache($courseid, false, true);
2863              }
2864          }
2865      }
2866  }
2867  
2868  /**
2869   * Add LTI Type course category.
2870   *
2871   * @param int $typeid
2872   * @param string $lticoursecategories Comma separated list of course categories.
2873   * @return void
2874   */
2875  function lti_type_add_categories(int $typeid, string $lticoursecategories = '') : void {
2876      global $DB;
2877      $coursecategories = explode(',', $lticoursecategories);
2878      foreach ($coursecategories as $coursecategory) {
2879          $DB->insert_record('lti_types_categories', ['typeid' => $typeid, 'categoryid' => $coursecategory]);
2880      }
2881  }
2882  
2883  function lti_add_type($type, $config) {
2884      global $USER, $SITE, $DB;
2885  
2886      lti_prepare_type_for_save($type, $config);
2887  
2888      if (!isset($type->state)) {
2889          $type->state = LTI_TOOL_STATE_PENDING;
2890      }
2891  
2892      if (!isset($type->ltiversion)) {
2893          $type->ltiversion = LTI_VERSION_1;
2894      }
2895  
2896      if (!isset($type->timecreated)) {
2897          $type->timecreated = time();
2898      }
2899  
2900      if (!isset($type->createdby)) {
2901          $type->createdby = $USER->id;
2902      }
2903  
2904      if (!isset($type->course)) {
2905          $type->course = $SITE->id;
2906      }
2907  
2908      // Create a salt value to be used for signing passed data to extension services
2909      // The outcome service uses the service salt on the instance. This can be used
2910      // for communication with services not related to a specific LTI instance.
2911      $config->lti_servicesalt = uniqid('', true);
2912  
2913      $id = $DB->insert_record('lti_types', $type);
2914  
2915      if ($id) {
2916          foreach ($config as $key => $value) {
2917              if (!is_null($value)) {
2918                  if (substr($key, 0, 4) === 'lti_') {
2919                      $fieldname = substr($key, 4);
2920                  } else if (substr($key, 0, 11) !== 'ltiservice_') {
2921                      continue;
2922                  } else {
2923                      $fieldname = $key;
2924                  }
2925  
2926                  $record = new \StdClass();
2927                  $record->typeid = $id;
2928                  $record->name = $fieldname;
2929                  $record->value = $value;
2930  
2931                  lti_add_config($record);
2932              }
2933          }
2934          if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) {
2935              lti_type_add_categories($id, $config->lti_coursecategories);
2936          }
2937      }
2938  
2939      return $id;
2940  }
2941  
2942  /**
2943   * Given an array of tool proxies, filter them based on their state
2944   *
2945   * @param array $toolproxies An array of lti_tool_proxies records
2946   * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants
2947   *
2948   * @return array
2949   */
2950  function lti_filter_tool_proxy_types(array $toolproxies, $state) {
2951      $return = array();
2952      foreach ($toolproxies as $key => $toolproxy) {
2953          if ($toolproxy->state == $state) {
2954              $return[$key] = $toolproxy;
2955          }
2956      }
2957      return $return;
2958  }
2959  
2960  /**
2961   * Get the tool proxy instance given its GUID
2962   *
2963   * @param string  $toolproxyguid   Tool proxy GUID value
2964   *
2965   * @return object
2966   */
2967  function lti_get_tool_proxy_from_guid($toolproxyguid) {
2968      global $DB;
2969  
2970      $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid));
2971  
2972      return $toolproxy;
2973  }
2974  
2975  /**
2976   * Get the tool proxy instance given its registration URL
2977   *
2978   * @param string $regurl Tool proxy registration URL
2979   *
2980   * @return array The record of the tool proxy with this url
2981   */
2982  function lti_get_tool_proxies_from_registration_url($regurl) {
2983      global $DB;
2984  
2985      return $DB->get_records_sql(
2986          'SELECT * FROM {lti_tool_proxies}
2987          WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl',
2988          array('regurl' => $regurl)
2989      );
2990  }
2991  
2992  /**
2993   * Generates some of the tool proxy configuration based on the admin configuration details
2994   *
2995   * @param int $id
2996   *
2997   * @return mixed Tool Proxy details
2998   */
2999  function lti_get_tool_proxy($id) {
3000      global $DB;
3001  
3002      $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id));
3003      return $toolproxy;
3004  }
3005  
3006  /**
3007   * Returns lti tool proxies.
3008   *
3009   * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them
3010   * @return array of basicLTI types
3011   */
3012  function lti_get_tool_proxies($orphanedonly) {
3013      global $DB;
3014  
3015      if ($orphanedonly) {
3016          $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));
3017          $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3018          foreach ($proxies as $key => $value) {
3019              if (in_array($value->id, $usedproxyids)) {
3020                  unset($proxies[$key]);
3021              }
3022          }
3023          return $proxies;
3024      } else {
3025          return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3026      }
3027  }
3028  
3029  /**
3030   * Generates some of the tool proxy configuration based on the admin configuration details
3031   *
3032   * @param int $id
3033   *
3034   * @return mixed  Tool Proxy details
3035   */
3036  function lti_get_tool_proxy_config($id) {
3037      $toolproxy = lti_get_tool_proxy($id);
3038  
3039      $tp = new \stdClass();
3040      $tp->lti_registrationname = $toolproxy->name;
3041      $tp->toolproxyid = $toolproxy->id;
3042      $tp->state = $toolproxy->state;
3043      $tp->lti_registrationurl = $toolproxy->regurl;
3044      $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered);
3045      $tp->lti_services = explode("\n", $toolproxy->serviceoffered);
3046  
3047      return $tp;
3048  }
3049  
3050  /**
3051   * Update the database with a tool proxy instance
3052   *
3053   * @param object   $config    Tool proxy definition
3054   *
3055   * @return int  Record id number
3056   */
3057  function lti_add_tool_proxy($config) {
3058      global $USER, $DB;
3059  
3060      $toolproxy = new \stdClass();
3061      if (isset($config->lti_registrationname)) {
3062          $toolproxy->name = trim($config->lti_registrationname);
3063      }
3064      if (isset($config->lti_registrationurl)) {
3065          $toolproxy->regurl = trim($config->lti_registrationurl);
3066      }
3067      if (isset($config->lti_capabilities)) {
3068          $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities);
3069      } else {
3070          $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities()));
3071      }
3072      if (isset($config->lti_services)) {
3073          $toolproxy->serviceoffered = implode("\n", $config->lti_services);
3074      } else {
3075          $func = function($s) {
3076              return $s->get_id();
3077          };
3078          $servicenames = array_map($func, lti_get_services());
3079          $toolproxy->serviceoffered = implode("\n", $servicenames);
3080      }
3081      if (isset($config->toolproxyid) && !empty($config->toolproxyid)) {
3082          $toolproxy->id = $config->toolproxyid;
3083          if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) {
3084              $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3085              $toolproxy->guid = random_string();
3086              $toolproxy->secret = random_string();
3087          }
3088          $id = lti_update_tool_proxy($toolproxy);
3089      } else {
3090          $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3091          $toolproxy->timemodified = time();
3092          $toolproxy->timecreated = $toolproxy->timemodified;
3093          if (!isset($toolproxy->createdby)) {
3094              $toolproxy->createdby = $USER->id;
3095          }
3096          $toolproxy->guid = random_string();
3097          $toolproxy->secret = random_string();
3098          $id = $DB->insert_record('lti_tool_proxies', $toolproxy);
3099      }
3100  
3101      return $id;
3102  }
3103  
3104  /**
3105   * Updates a tool proxy in the database
3106   *
3107   * @param object  $toolproxy   Tool proxy
3108   *
3109   * @return int    Record id number
3110   */
3111  function lti_update_tool_proxy($toolproxy) {
3112      global $DB;
3113  
3114      $toolproxy->timemodified = time();
3115      $id = $DB->update_record('lti_tool_proxies', $toolproxy);
3116  
3117      return $id;
3118  }
3119  
3120  /**
3121   * Delete a Tool Proxy
3122   *
3123   * @param int $id   Tool Proxy id
3124   */
3125  function lti_delete_tool_proxy($id) {
3126      global $DB;
3127      $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id));
3128      $tools = $DB->get_records('lti_types', array('toolproxyid' => $id));
3129      foreach ($tools as $tool) {
3130          lti_delete_type($tool->id);
3131      }
3132      $DB->delete_records('lti_tool_proxies', array('id' => $id));
3133  }
3134  
3135  /**
3136   * Get both LTI tool proxies and tool types.
3137   *
3138   * If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool
3139   * types.
3140   * For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types
3141   * will be returned.
3142   *
3143   * @param int $limit Maximum number of tools returned.
3144   * @param int $offset Do not return tools before offset index.
3145   * @param bool $orphanedonly If true, only return orphaned proxies.
3146   * @param int $toolproxyid If not 0, only return tool types that have this tool proxy id.
3147   * @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types.
3148   */
3149  function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array {
3150      global $DB;
3151  
3152      if ($orphanedonly) {
3153          $orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false);
3154          $countsql = helper::get_tool_proxy_sql($orphanedonly, true);
3155          $proxies  = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit);
3156          $totalproxiescount = $DB->count_records_sql($countsql);
3157      } else {
3158          $proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC',
3159              '*', $offset, $limit);
3160          $totalproxiescount = $DB->count_records('lti_tool_proxies');
3161      }
3162  
3163      // Find new offset and limit for tool types after getting proxies and set up query.
3164      $typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative.
3165      $typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative.
3166      $typesparams = [];
3167      if (!empty($toolproxyid)) {
3168          $typesparams['toolproxyid'] = $toolproxyid;
3169      }
3170  
3171      $types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC',
3172              '*', $typesoffset, $typeslimit);
3173  
3174      return [$proxies, array_map('serialise_tool_type', $types)];
3175  }
3176  
3177  /**
3178   * Get the total number of LTI tool types and tool proxies.
3179   *
3180   * @param bool $orphanedonly If true, only count orphaned proxies.
3181   * @param int $toolproxyid If not 0, only count tool types that have this tool proxy id.
3182   * @return int Count of tools.
3183   */
3184  function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int {
3185      global $DB;
3186  
3187      $typessql = "SELECT count(*)
3188                     FROM {lti_types}";
3189      $typesparams = [];
3190      if (!empty($toolproxyid)) {
3191          $typessql .= " WHERE toolproxyid = :toolproxyid";
3192          $typesparams['toolproxyid'] = $toolproxyid;
3193      }
3194  
3195      $proxiessql = helper::get_tool_proxy_sql($orphanedonly, true);
3196  
3197      $countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause();
3198  
3199      return $DB->count_records_sql($countsql, $typesparams);
3200  }
3201  
3202  /**
3203   * Add a tool configuration in the database
3204   *
3205   * @param object $config   Tool configuration
3206   *
3207   * @return int Record id number
3208   */
3209  function lti_add_config($config) {
3210      global $DB;
3211  
3212      return $DB->insert_record('lti_types_config', $config);
3213  }
3214  
3215  /**
3216   * Updates a tool configuration in the database
3217   *
3218   * @param object  $config   Tool configuration
3219   *
3220   * @return mixed Record id number
3221   */
3222  function lti_update_config($config) {
3223      global $DB;
3224  
3225      $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));
3226  
3227      if ($old) {
3228          $config->id = $old->id;
3229          $return = $DB->update_record('lti_types_config', $config);
3230      } else {
3231          $return = $DB->insert_record('lti_types_config', $config);
3232      }
3233      return $return;
3234  }
3235  
3236  /**
3237   * Gets the tool settings
3238   *
3239   * @param int  $toolproxyid   Id of tool proxy record (or tool ID if negative)
3240   * @param int  $courseid      Id of course (null if system settings)
3241   * @param int  $instanceid    Id of course module (null if system or context settings)
3242   *
3243   * @return array  Array settings
3244   */
3245  function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) {
3246      global $DB;
3247  
3248      $settings = array();
3249      if ($toolproxyid > 0) {
3250          $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
3251              'course' => $courseid, 'coursemoduleid' => $instanceid));
3252      } else {
3253          $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid,
3254              'course' => $courseid, 'coursemoduleid' => $instanceid));
3255      }
3256      if ($settingsstr !== false) {
3257          $settings = json_decode($settingsstr, true);
3258      }
3259      return $settings;
3260  }
3261  
3262  /**
3263   * Sets the tool settings (
3264   *
3265   * @param array  $settings      Array of settings
3266   * @param int    $toolproxyid   Id of tool proxy record (or tool ID if negative)
3267   * @param int    $courseid      Id of course (null if system settings)
3268   * @param int    $instanceid    Id of course module (null if system or context settings)
3269   */
3270  function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) {
3271      global $DB;
3272  
3273      $json = json_encode($settings);
3274      if ($toolproxyid >= 0) {
3275          $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
3276              'course' => $courseid, 'coursemoduleid' => $instanceid));
3277      } else {
3278          $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid,
3279              'course' => $courseid, 'coursemoduleid' => $instanceid));
3280      }
3281      if ($record !== false) {
3282          $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
3283      } else {
3284          $record = new \stdClass();
3285          if ($toolproxyid > 0) {
3286              $record->toolproxyid = $toolproxyid;
3287          } else {
3288              $record->typeid = -$toolproxyid;
3289          }
3290          $record->course = $courseid;
3291          $record->coursemoduleid = $instanceid;
3292          $record->settings = $json;
3293          $record->timecreated = time();
3294          $record->timemodified = $record->timecreated;
3295          $DB->insert_record('lti_tool_settings', $record);
3296      }
3297  }
3298  
3299  /**
3300   * Signs the petition to launch the external tool using OAuth
3301   *
3302   * @param array  $oldparms     Parameters to be passed for signing
3303   * @param string $endpoint     url of the external tool
3304   * @param string $method       Method for sending the parameters (e.g. POST)
3305   * @param string $oauthconsumerkey
3306   * @param string $oauthconsumersecret
3307   * @return array|null
3308   */
3309  function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
3310  
3311      $parms = $oldparms;
3312  
3313      $testtoken = '';
3314  
3315      // TODO: Switch to core oauthlib once implemented - MDL-30149.
3316      $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1();
3317      $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null);
3318      $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms);
3319      $accreq->sign_request($hmacmethod, $testconsumer, $testtoken);
3320  
3321      $newparms = $accreq->get_parameters();
3322  
3323      return $newparms;
3324  }
3325  
3326  /**
3327   * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT
3328   *
3329   * @param array  $parms        Parameters to be passed for signing
3330   * @param string $endpoint     url of the external tool
3331   * @param string $oauthconsumerkey
3332   * @param string $typeid       ID of LTI tool type
3333   * @param string $nonce        Nonce value to use
3334   * @return array|null
3335   */
3336  function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {
3337      global $CFG;
3338  
3339      if (empty($typeid)) {
3340          $typeid = 0;
3341      }
3342      $messagetypemapping = lti_get_jwt_message_type_mapping();
3343      if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) {
3344          $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']];
3345      }
3346      if (isset($parms['roles'])) {
3347          $roles = explode(',', $parms['roles']);
3348          $newroles = array();
3349          foreach ($roles as $role) {
3350              if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {
3351                  $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);
3352              } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {
3353                  $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);
3354              } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {
3355                  $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);
3356              } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {
3357                  $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";
3358              }
3359              $newroles[] = $role;
3360          }
3361          $parms['roles'] = implode(',', $newroles);
3362      }
3363  
3364      $now = time();
3365      if (empty($nonce)) {
3366          $nonce = bin2hex(openssl_random_pseudo_bytes(10));
3367      }
3368      $claimmapping = lti_get_jwt_claim_mapping();
3369      $payload = array(
3370          'nonce' => $nonce,
3371          'iat' => $now,
3372          'exp' => $now + 60,
3373      );
3374      $payload['iss'] = $CFG->wwwroot;
3375      $payload['aud'] = $oauthconsumerkey;
3376      $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid);
3377      $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;
3378  
3379      foreach ($parms as $key => $value) {
3380          $claim = LTI_JWT_CLAIM_PREFIX;
3381          if (array_key_exists($key, $claimmapping)) {
3382              $mapping = $claimmapping[$key];
3383              $type = $mapping["type"] ?? "string";
3384              if ($mapping['isarray']) {
3385                  $value = explode(',', $value);
3386                  sort($value);
3387              } else if ($type == 'boolean') {
3388                  $value = isset($value) && ($value == 'true');
3389              }
3390              if (!empty($mapping['suffix'])) {
3391                  $claim .= "-{$mapping['suffix']}";
3392              }
3393              $claim .= '/claim/';
3394              if (is_null($mapping['group'])) {
3395                  $payload[$mapping['claim']] = $value;
3396              } else if (empty($mapping['group'])) {
3397                  $payload["{$claim}{$mapping['claim']}"] = $value;
3398              } else {
3399                  $claim .= $mapping['group'];
3400                  $payload[$claim][$mapping['claim']] = $value;
3401              }
3402          } else if (strpos($key, 'custom_') === 0) {
3403              $payload["{$claim}/claim/custom"][substr($key, 7)] = $value;
3404          } else if (strpos($key, 'ext_') === 0) {
3405              $payload["{$claim}/claim/ext"][substr($key, 4)] = $value;
3406          }
3407      }
3408  
3409      $privatekey = jwks_helper::get_private_key();
3410      $jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']);
3411  
3412      $newparms = array();
3413      $newparms['id_token'] = $jwt;
3414  
3415      return $newparms;
3416  }
3417  
3418  /**
3419   * Verfies the JWT and converts its claims to their equivalent message parameter.
3420   *
3421   * @param int    $typeid
3422   * @param string $jwtparam   JWT parameter
3423   *
3424   * @return array  message parameters
3425   * @throws moodle_exception
3426   */
3427  function lti_convert_from_jwt($typeid, $jwtparam) {
3428  
3429      $params = array();
3430      $parts = explode('.', $jwtparam);
3431      $ok = (count($parts) === 3);
3432      if ($ok) {
3433          $payload = JWT::urlsafeB64Decode($parts[1]);
3434          $claims = json_decode($payload, true);
3435          $ok = !is_null($claims) && !empty($claims['iss']);
3436      }
3437      if ($ok) {
3438          lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);
3439          $params['oauth_consumer_key'] = $claims['iss'];
3440          foreach (lti_get_jwt_claim_mapping() as $key => $mapping) {
3441              $claim = LTI_JWT_CLAIM_PREFIX;
3442              if (!empty($mapping['suffix'])) {
3443                  $claim .= "-{$mapping['suffix']}";
3444              }
3445              $claim .= '/claim/';
3446              if (is_null($mapping['group'])) {
3447                  $claim = $mapping['claim'];
3448              } else if (empty($mapping['group'])) {
3449                  $claim .= $mapping['claim'];
3450              } else {
3451                  $claim .= $mapping['group'];
3452              }
3453              if (isset($claims[$claim])) {
3454                  $value = null;
3455                  if (empty($mapping['group'])) {
3456                      $value = $claims[$claim];
3457                  } else {
3458                      $group = $claims[$claim];
3459                      if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
3460                          $value = $group[$mapping['claim']];
3461                      }
3462                  }
3463                  if (!empty($value) && $mapping['isarray']) {
3464                      if (is_array($value)) {
3465                          if (is_array($value[0])) {
3466                              $value = json_encode($value);
3467                          } else {
3468                              $value = implode(',', $value);
3469                          }
3470                      }
3471                  }
3472                  if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {
3473                      $params[$key] = $value;
3474                  }
3475              }
3476              $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';
3477              if (isset($claims[$claim])) {
3478                  $custom = $claims[$claim];
3479                  if (is_array($custom)) {
3480                      foreach ($custom as $key => $value) {
3481                          $params["custom_{$key}"] = $value;
3482                      }
3483                  }
3484              }
3485              $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';
3486              if (isset($claims[$claim])) {
3487                  $ext = $claims[$claim];
3488                  if (is_array($ext)) {
3489                      foreach ($ext as $key => $value) {
3490                          $params["ext_{$key}"] = $value;
3491                      }
3492                  }
3493              }
3494          }
3495      }
3496      if (isset($params['content_items'])) {
3497          $params['content_items'] = lti_convert_content_items($params['content_items']);
3498      }
3499      $messagetypemapping = lti_get_jwt_message_type_mapping();
3500      if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) {
3501          $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']];
3502      }
3503      return $params;
3504  }
3505  
3506  /**
3507   * Posts the launch petition HTML
3508   *
3509   * @param array $newparms   Signed parameters
3510   * @param string $endpoint  URL of the external tool
3511   * @param bool $debug       Debug (true/false)
3512   * @return string
3513   */
3514  function lti_post_launch_html($newparms, $endpoint, $debug=false) {
3515      $r = "<form action=\"" . $endpoint .
3516          "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n";
3517  
3518      // Contruct html for the launch parameters.
3519      foreach ($newparms as $key => $value) {
3520          $key = htmlspecialchars($key, ENT_COMPAT);
3521          $value = htmlspecialchars($value, ENT_COMPAT);
3522          if ( $key == "ext_submit" ) {
3523              $r .= "<input type=\"submit\"";
3524          } else {
3525              $r .= "<input type=\"hidden\" name=\"{$key}\"";
3526          }
3527          $r .= " value=\"";
3528          $r .= $value;
3529          $r .= "\"/>\n";
3530      }
3531  
3532      if ( $debug ) {
3533          $r .= "<script language=\"javascript\"> \n";
3534          $r .= "  //<![CDATA[ \n";
3535          $r .= "function basicltiDebugToggle() {\n";
3536          $r .= "    var ele = document.getElementById(\"basicltiDebug\");\n";
3537          $r .= "    if (ele.style.display == \"block\") {\n";
3538          $r .= "        ele.style.display = \"none\";\n";
3539          $r .= "    }\n";
3540          $r .= "    else {\n";
3541          $r .= "        ele.style.display = \"block\";\n";
3542          $r .= "    }\n";
3543          $r .= "} \n";
3544          $r .= "  //]]> \n";
3545          $r .= "</script>\n";
3546          $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";
3547          $r .= get_string("toggle_debug_data", "lti")."</a>\n";
3548          $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
3549          $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n";
3550          $r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
3551          $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n";
3552          foreach ($newparms as $key => $value) {
3553              $key = htmlspecialchars($key, ENT_COMPAT);
3554              $value = htmlspecialchars($value, ENT_COMPAT);
3555              $r .= "$key = $value<br/>\n";
3556          }
3557          $r .= "&nbsp;<br/>\n";
3558          $r .= "</div>\n";
3559      }
3560      $r .= "</form>\n";
3561  
3562      // Auto-submit the form if endpoint is set.
3563      if ($endpoint !== '' && !$debug) {
3564          $r .= " <script type=\"text/javascript\"> \n" .
3565              "  //<![CDATA[ \n" .
3566              "    document.ltiLaunchForm.submit(); \n" .
3567              "  //]]> \n" .
3568              " </script> \n";
3569      }
3570      return $r;
3571  }
3572  
3573  /**
3574   * Generate the form for initiating a login request for an LTI 1.3 message
3575   *
3576   * @param int            $courseid  Course ID
3577   * @param int            $cmid        LTI instance ID
3578   * @param stdClass|null  $instance  LTI instance
3579   * @param stdClass       $config    Tool type configuration
3580   * @param string         $messagetype   LTI message type
3581   * @param string         $title     Title of content item
3582   * @param string         $text      Description of content item
3583   * @param int            $foruserid Id of the user targeted by the launch
3584   * @return string
3585   */
3586  function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request',
3587          $title = '', $text = '', $foruserid = 0) {
3588      global $SESSION;
3589  
3590      $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text);
3591  
3592      $r = "<form action=\"" . $config->lti_initiatelogin .
3593          "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " .
3594          "encType=\"application/x-www-form-urlencoded\">\n";
3595  
3596      foreach ($params as $key => $value) {
3597          $key = htmlspecialchars($key, ENT_COMPAT);
3598          $value = htmlspecialchars($value, ENT_COMPAT);
3599          $r .= "  <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
3600      }
3601      $r .= "</form>\n";
3602  
3603      $r .= "<script type=\"text/javascript\">\n" .
3604          "//<![CDATA[\n" .
3605          "document.ltiInitiateLoginForm.submit();\n" .
3606          "//]]>\n" .
3607          "</script>\n";
3608  
3609      return $r;
3610  }
3611  
3612  /**
3613   * Prepares an LTI 1.3 login request
3614   *
3615   * @param int            $courseid  Course ID
3616   * @param int            $cmid        Course Module instance ID
3617   * @param stdClass|null  $instance  LTI instance
3618   * @param stdClass       $config    Tool type configuration
3619   * @param string         $messagetype   LTI message type
3620   * @param int            $foruserid Id of the user targeted by the launch
3621   * @param string         $title     Title of content item
3622   * @param string         $text      Description of content item
3623   * @return array Login request parameters
3624   */
3625  function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') {
3626      global $USER, $CFG, $SESSION;
3627      $ltihint = [];
3628      if (!empty($instance)) {
3629          $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;
3630          $launchid = 'ltilaunch'.$instance->id.'_'.rand();
3631          $ltihint['cmid'] = $cmid;
3632          $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,";
3633      } else {
3634          $endpoint = $config->lti_toolurl;
3635          if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) {
3636              $endpoint = $config->lti_toolurl_ContentItemSelectionRequest;
3637          }
3638          $launchid = "ltilaunch_$messagetype".rand();
3639          $SESSION->$launchid =
3640              "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text);
3641      }
3642      $endpoint = trim($endpoint);
3643      $services = lti_get_services();
3644      foreach ($services as $service) {
3645          [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance);
3646      }
3647  
3648      $ltihint['launchid'] = $launchid;
3649      // If SSL is forced make sure https is on the normal launch URL.
3650      if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {
3651          $endpoint = lti_ensure_url_is_https($endpoint);
3652      } else if (!strstr($endpoint, '://')) {
3653          $endpoint = 'http://' . $endpoint;
3654      }
3655  
3656      $params = array();
3657      $params['iss'] = $CFG->wwwroot;
3658      $params['target_link_uri'] = $endpoint;
3659      $params['login_hint'] = $USER->id;
3660      $params['lti_message_hint'] = json_encode($ltihint);
3661      $params['client_id'] = $config->lti_clientid;
3662      $params['lti_deployment_id'] = $config->typeid;
3663      return $params;
3664  }
3665  
3666  function lti_get_type($typeid) {
3667      global $DB;
3668  
3669      return $DB->get_record('lti_types', array('id' => $typeid));
3670  }
3671  
3672  function lti_get_launch_container($lti, $toolconfig) {
3673      if (empty($lti->launchcontainer)) {
3674          $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
3675      }
3676  
3677      if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3678          if (isset($toolconfig['launchcontainer'])) {
3679              $launchcontainer = $toolconfig['launchcontainer'];
3680          }
3681      } else {
3682          $launchcontainer = $lti->launchcontainer;
3683      }
3684  
3685      if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3686          $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
3687      }
3688  
3689      $devicetype = core_useragent::get_device_type();
3690  
3691      // Scrolling within the object element doesn't work on iOS or Android
3692      // Opening the popup window also had some issues in testing
3693      // For mobile devices, always take up the entire screen to ensure the best experience.
3694      if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) {
3695          $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW;
3696      }
3697  
3698      return $launchcontainer;
3699  }
3700  
3701  function lti_request_is_using_ssl() {
3702      global $CFG;
3703      return (stripos($CFG->wwwroot, 'https://') === 0);
3704  }
3705  
3706  function lti_ensure_url_is_https($url) {
3707      if (!strstr($url, '://')) {
3708          $url = 'https://' . $url;
3709      } else {
3710          // If the URL starts with http, replace with https.
3711          if (stripos($url, 'http://') === 0) {
3712              $url = 'https://' . substr($url, 7);
3713          }
3714      }
3715  
3716      return $url;
3717  }
3718  
3719  /**
3720   * Determines if we should try to log the request
3721   *
3722   * @param string $rawbody
3723   * @return bool
3724   */
3725  function lti_should_log_request($rawbody) {
3726      global $CFG;
3727  
3728      if (empty($CFG->mod_lti_log_users)) {
3729          return false;
3730      }
3731  
3732      $logusers = explode(',', $CFG->mod_lti_log_users);
3733      if (empty($logusers)) {
3734          return false;
3735      }
3736  
3737      try {
3738          $xml = new \SimpleXMLElement($rawbody);
3739          $ns  = $xml->getNamespaces();
3740          $ns  = array_shift($ns);
3741          $xml->registerXPathNamespace('lti', $ns);
3742          $requestuserid = '';
3743          if ($node = $xml->xpath('//lti:userId')) {
3744              $node = $node[0];
3745              $requestuserid = clean_param((string) $node, PARAM_INT);
3746          } else if ($node = $xml->xpath('//lti:sourcedId')) {
3747              $node = $node[0];
3748              $resultjson = json_decode((string) $node);
3749              $requestuserid = clean_param($resultjson->data->userid, PARAM_INT);
3750          }
3751      } catch (Exception $e) {
3752          return false;
3753      }
3754  
3755      if (empty($requestuserid) or !in_array($requestuserid, $logusers)) {
3756          return false;
3757      }
3758  
3759      return true;
3760  }
3761  
3762  /**
3763   * Logs the request to a file in temp dir.
3764   *
3765   * @param string $rawbody
3766   */
3767  function lti_log_request($rawbody) {
3768      if ($tempdir = make_temp_directory('mod_lti', false)) {
3769          if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
3770              $content  = "Request Headers:\n";
3771              foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
3772                  $content .= "$header: $value\n";
3773              }
3774              $content .= "Request Body:\n";
3775              $content .= $rawbody;
3776  
3777              file_put_contents($tempfile, $content);
3778              chmod($tempfile, 0644);
3779          }
3780      }
3781  }
3782  
3783  /**
3784   * Log an LTI response.
3785   *
3786   * @param string $responsexml The response XML
3787   * @param Exception $e If there was an exception, pass that too
3788   */
3789  function lti_log_response($responsexml, $e = null) {
3790      if ($tempdir = make_temp_directory('mod_lti', false)) {
3791          if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
3792              $content = '';
3793              if ($e instanceof Exception) {
3794                  $info = get_exception_info($e);
3795  
3796                  $content .= "Exception:\n";
3797                  $content .= "Message: $info->message\n";
3798                  $content .= "Debug info: $info->debuginfo\n";
3799                  $content .= "Backtrace:\n";
3800                  $content .= format_backtrace($info->backtrace, true);
3801                  $content .= "\n";
3802              }
3803              $content .= "Response XML:\n";
3804              $content .= $responsexml;
3805  
3806              file_put_contents($tempfile, $content);
3807              chmod($tempfile, 0644);
3808          }
3809      }
3810  }
3811  
3812  /**
3813   * Fetches LTI type configuration for an LTI instance
3814   *
3815   * @param stdClass $instance
3816   * @return array Can be empty if no type is found
3817   */
3818  function lti_get_type_config_by_instance($instance) {
3819      $typeid = null;
3820      if (empty($instance->typeid)) {
3821          $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
3822          if ($tool) {
3823              $typeid = $tool->id;
3824          }
3825      } else {
3826          $typeid = $instance->typeid;
3827      }
3828      if (!empty($typeid)) {
3829          return lti_get_type_config($typeid);
3830      }
3831      return array();
3832  }
3833  
3834  /**
3835   * Enforce type config settings onto the LTI instance
3836   *
3837   * @param stdClass $instance
3838   * @param array $typeconfig
3839   */
3840  function lti_force_type_config_settings($instance, array $typeconfig) {
3841      $forced = array(
3842          'instructorchoicesendname'      => 'sendname',
3843          'instructorchoicesendemailaddr' => 'sendemailaddr',
3844          'instructorchoiceacceptgrades'  => 'acceptgrades',
3845      );
3846  
3847      foreach ($forced as $instanceparam => $typeconfigparam) {
3848          if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) {
3849              $instance->$instanceparam = $typeconfig[$typeconfigparam];
3850          }
3851      }
3852  }
3853  
3854  /**
3855   * Initializes an array with the capabilities supported by the LTI module
3856   *
3857   * @return array List of capability names (without a dollar sign prefix)
3858   */
3859  function lti_get_capabilities() {
3860  
3861      $capabilities = array(
3862         'basic-lti-launch-request' => '',
3863         'ContentItemSelectionRequest' => '',
3864         'ToolProxyRegistrationRequest' => '',
3865         'Context.id' => 'context_id',
3866         'Context.title' => 'context_title',
3867         'Context.label' => 'context_label',
3868         'Context.id.history' => null,
3869         'Context.sourcedId' => 'lis_course_section_sourcedid',
3870         'Context.longDescription' => '$COURSE->summary',
3871         'Context.timeFrame.begin' => '$COURSE->startdate',
3872         'CourseSection.title' => 'context_title',
3873         'CourseSection.label' => 'context_label',
3874         'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
3875         'CourseSection.longDescription' => '$COURSE->summary',
3876         'CourseSection.timeFrame.begin' => null,
3877         'CourseSection.timeFrame.end' => null,
3878         'ResourceLink.id' => 'resource_link_id',
3879         'ResourceLink.title' => 'resource_link_title',
3880         'ResourceLink.description' => 'resource_link_description',
3881         'User.id' => 'user_id',
3882         'User.username' => '$USER->username',
3883         'Person.name.full' => 'lis_person_name_full',
3884         'Person.name.given' => 'lis_person_name_given',
3885         'Person.name.family' => 'lis_person_name_family',
3886         'Person.email.primary' => 'lis_person_contact_email_primary',
3887         'Person.sourcedId' => 'lis_person_sourcedid',
3888         'Person.name.middle' => '$USER->middlename',
3889         'Person.address.street1' => '$USER->address',
3890         'Person.address.locality' => '$USER->city',
3891         'Person.address.country' => '$USER->country',
3892         'Person.address.timezone' => '$USER->timezone',
3893         'Person.phone.primary' => '$USER->phone1',
3894         'Person.phone.mobile' => '$USER->phone2',
3895         'Person.webaddress' => '$USER->url',
3896         'Membership.role' => 'roles',
3897         'Result.sourcedId' => 'lis_result_sourcedid',
3898         'Result.autocreate' => 'lis_outcome_service_url',
3899         'BasicOutcome.sourcedId' => 'lis_result_sourcedid',
3900         'BasicOutcome.url' => 'lis_outcome_service_url',
3901         'Moodle.Person.userGroupIds' => null);
3902  
3903      return $capabilities;
3904  
3905  }
3906  
3907  /**
3908   * Initializes an array with the services supported by the LTI module
3909   *
3910   * @return array List of services
3911   */
3912  function lti_get_services() {
3913  
3914      $services = array();
3915      $definedservices = core_component::get_plugin_list('ltiservice');
3916      foreach ($definedservices as $name => $location) {
3917          $classname = "\\ltiservice_{$name}\\local\\service\\{$name}";
3918          $services[] = new $classname();
3919      }
3920  
3921      return $services;
3922  
3923  }
3924  
3925  /**
3926   * Initializes an instance of the named service
3927   *
3928   * @param string $servicename Name of service
3929   *
3930   * @return bool|\mod_lti\local\ltiservice\service_base Service
3931   */
3932  function lti_get_service_by_name($servicename) {
3933  
3934      $service = false;
3935      $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}";
3936      if (class_exists($classname)) {
3937          $service = new $classname();
3938      }
3939  
3940      return $service;
3941  
3942  }
3943  
3944  /**
3945   * Finds a service by id
3946   *
3947   * @param \mod_lti\local\ltiservice\service_base[] $services Array of services
3948   * @param string $resourceid  ID of resource
3949   *
3950   * @return mod_lti\local\ltiservice\service_base Service
3951   */
3952  function lti_get_service_by_resource_id($services, $resourceid) {
3953  
3954      $service = false;
3955      foreach ($services as $aservice) {
3956          foreach ($aservice->get_resources() as $resource) {
3957              if ($resource->get_id() === $resourceid) {
3958                  $service = $aservice;
3959                  break 2;
3960              }
3961          }
3962      }
3963  
3964      return $service;
3965  
3966  }
3967  
3968  /**
3969   * Initializes an array with the scopes for services supported by the LTI module
3970   * and authorized for this particular tool instance.
3971   *
3972   * @param object $type  LTI tool type
3973   * @param array  $typeconfig  LTI tool type configuration
3974   *
3975   * @return array List of scopes
3976   */
3977  function lti_get_permitted_service_scopes($type, $typeconfig) {
3978  
3979      $services = lti_get_services();
3980      $scopes = array();
3981      foreach ($services as $service) {
3982          $service->set_type($type);
3983          $service->set_typeconfig($typeconfig);
3984          $servicescopes = $service->get_permitted_scopes();
3985          if (!empty($servicescopes)) {
3986              $scopes = array_merge($scopes, $servicescopes);
3987          }
3988      }
3989  
3990      return $scopes;
3991  }
3992  
3993  /**
3994   * Extracts the named contexts from a tool proxy
3995   *
3996   * @param object $json
3997   *
3998   * @return array Contexts
3999   */
4000  function lti_get_contexts($json) {
4001  
4002      $contexts = array();
4003      if (isset($json->{'@context'})) {
4004          foreach ($json->{'@context'} as $context) {
4005              if (is_object($context)) {
4006                  $contexts = array_merge(get_object_vars($context), $contexts);
4007              }
4008          }
4009      }
4010  
4011      return $contexts;
4012  
4013  }
4014  
4015  /**
4016   * Converts an ID to a fully-qualified ID
4017   *
4018   * @param array $contexts
4019   * @param string $id
4020   *
4021   * @return string Fully-qualified ID
4022   */
4023  function lti_get_fqid($contexts, $id) {
4024  
4025      $parts = explode(':', $id, 2);
4026      if (count($parts) > 1) {
4027          if (array_key_exists($parts[0], $contexts)) {
4028              $id = $contexts[$parts[0]] . $parts[1];
4029          }
4030      }
4031  
4032      return $id;
4033  
4034  }
4035  
4036  /**
4037   * Returns the icon for the given tool type
4038   *
4039   * @param stdClass $type The tool type
4040   *
4041   * @return string The url to the tool type's corresponding icon
4042   */
4043  function get_tool_type_icon_url(stdClass $type) {
4044      global $OUTPUT;
4045  
4046      $iconurl = $type->secureicon;
4047  
4048      if (empty($iconurl)) {
4049          $iconurl = $type->icon;
4050      }
4051  
4052      if (empty($iconurl)) {
4053          $iconurl = $OUTPUT->image_url('monologo', 'lti')->out();
4054      }
4055  
4056      return $iconurl;
4057  }
4058  
4059  /**
4060   * Returns the edit url for the given tool type
4061   *
4062   * @param stdClass $type The tool type
4063   *
4064   * @return string The url to edit the tool type
4065   */
4066  function get_tool_type_edit_url(stdClass $type) {
4067      $url = new moodle_url('/mod/lti/typessettings.php',
4068                            array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4069      return $url->out();
4070  }
4071  
4072  /**
4073   * Returns the edit url for the given tool proxy.
4074   *
4075   * @param stdClass $proxy The tool proxy
4076   *
4077   * @return string The url to edit the tool type
4078   */
4079  function get_tool_proxy_edit_url(stdClass $proxy) {
4080      $url = new moodle_url('/mod/lti/registersettings.php',
4081                            array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4082      return $url->out();
4083  }
4084  
4085  /**
4086   * Returns the course url for the given tool type
4087   *
4088   * @param stdClass $type The tool type
4089   *
4090   * @return string The url to the course of the tool type, void if it is a site wide type
4091   */
4092  function get_tool_type_course_url(stdClass $type) {
4093      if ($type->course != 1) {
4094          $url = new moodle_url('/course/view.php', array('id' => $type->course));
4095          return $url->out();
4096      }
4097      return null;
4098  }
4099  
4100  /**
4101   * Returns the icon and edit urls for the tool type and the course url if it is a course type.
4102   *
4103   * @param stdClass $type The tool type
4104   *
4105   * @return array The urls of the tool type
4106   */
4107  function get_tool_type_urls(stdClass $type) {
4108      $courseurl = get_tool_type_course_url($type);
4109  
4110      $urls = array(
4111          'icon' => get_tool_type_icon_url($type),
4112          'edit' => get_tool_type_edit_url($type),
4113      );
4114  
4115      if ($courseurl) {
4116          $urls['course'] = $courseurl;
4117      }
4118  
4119      $url = new moodle_url('/mod/lti/certs.php');
4120      $urls['publickeyset'] = $url->out();
4121      $url = new moodle_url('/mod/lti/token.php');
4122      $urls['accesstoken'] = $url->out();
4123      $url = new moodle_url('/mod/lti/auth.php');
4124      $urls['authrequest'] = $url->out();
4125  
4126      return $urls;
4127  }
4128  
4129  /**
4130   * Returns the icon and edit urls for the tool proxy.
4131   *
4132   * @param stdClass $proxy The tool proxy
4133   *
4134   * @return array The urls of the tool proxy
4135   */
4136  function get_tool_proxy_urls(stdClass $proxy) {
4137      global $OUTPUT;
4138  
4139      $urls = array(
4140          'icon' => $OUTPUT->image_url('monologo', 'lti')->out(),
4141          'edit' => get_tool_proxy_edit_url($proxy),
4142      );
4143  
4144      return $urls;
4145  }
4146  
4147  /**
4148   * Returns information on the current state of the tool type
4149   *
4150   * @param stdClass $type The tool type
4151   *
4152   * @return array An array with a text description of the state, and boolean for whether it is in each state:
4153   * pending, configured, rejected, unknown
4154   */
4155  function get_tool_type_state_info(stdClass $type) {
4156      $isconfigured = false;
4157      $ispending = false;
4158      $isrejected = false;
4159      $isunknown = false;
4160      switch ($type->state) {
4161          case LTI_TOOL_STATE_CONFIGURED:
4162              $state = get_string('active', 'mod_lti');
4163              $isconfigured = true;
4164              break;
4165          case LTI_TOOL_STATE_PENDING:
4166              $state = get_string('pending', 'mod_lti');
4167              $ispending = true;
4168              break;
4169          case LTI_TOOL_STATE_REJECTED:
4170              $state = get_string('rejected', 'mod_lti');
4171              $isrejected = true;
4172              break;
4173          default:
4174              $state = get_string('unknownstate', 'mod_lti');
4175              $isunknown = true;
4176              break;
4177      }
4178  
4179      return array(
4180          'text' => $state,
4181          'pending' => $ispending,
4182          'configured' => $isconfigured,
4183          'rejected' => $isrejected,
4184          'unknown' => $isunknown
4185      );
4186  }
4187  
4188  /**
4189   * Returns information on the configuration of the tool type
4190   *
4191   * @param stdClass $type The tool type
4192   *
4193   * @return array An array with configuration details
4194   */
4195  function get_tool_type_config($type) {
4196      global $CFG;
4197      $platformid = $CFG->wwwroot;
4198      $clientid = $type->clientid;
4199      $deploymentid = $type->id;
4200      $publickeyseturl = new moodle_url('/mod/lti/certs.php');
4201      $publickeyseturl = $publickeyseturl->out();
4202  
4203      $accesstokenurl = new moodle_url('/mod/lti/token.php');
4204      $accesstokenurl = $accesstokenurl->out();
4205  
4206      $authrequesturl = new moodle_url('/mod/lti/auth.php');
4207      $authrequesturl = $authrequesturl->out();
4208  
4209      return array(
4210          'platformid' => $platformid,
4211          'clientid' => $clientid,
4212          'deploymentid' => $deploymentid,
4213          'publickeyseturl' => $publickeyseturl,
4214          'accesstokenurl' => $accesstokenurl,
4215          'authrequesturl' => $authrequesturl
4216      );
4217  }
4218  
4219  /**
4220   * Returns a summary of each LTI capability this tool type requires in plain language
4221   *
4222   * @param stdClass $type The tool type
4223   *
4224   * @return array An array of text descriptions of each of the capabilities this tool type requires
4225   */
4226  function get_tool_type_capability_groups($type) {
4227      $capabilities = lti_get_enabled_capabilities($type);
4228      $groups = array();
4229      $hascourse = false;
4230      $hasactivities = false;
4231      $hasuseraccount = false;
4232      $hasuserpersonal = false;
4233  
4234      foreach ($capabilities as $capability) {
4235          // Bail out early if we've already found all groups.
4236          if (count($groups) >= 4) {
4237              continue;
4238          }
4239  
4240          if (!$hascourse && preg_match('/^CourseSection/', $capability)) {
4241              $hascourse = true;
4242              $groups[] = get_string('courseinformation', 'mod_lti');
4243          } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) {
4244              $hasactivities = true;
4245              $groups[] = get_string('courseactivitiesorresources', 'mod_lti');
4246          } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) {
4247              $hasuseraccount = true;
4248              $groups[] = get_string('useraccountinformation', 'mod_lti');
4249          } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) {
4250              $hasuserpersonal = true;
4251              $groups[] = get_string('userpersonalinformation', 'mod_lti');
4252          }
4253      }
4254  
4255      return $groups;
4256  }
4257  
4258  
4259  /**
4260   * Returns the ids of each instance of this tool type
4261   *
4262   * @param stdClass $type The tool type
4263   *
4264   * @return array An array of ids of the instances of this tool type
4265   */
4266  function get_tool_type_instance_ids($type) {
4267      global $DB;
4268  
4269      return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id)));
4270  }
4271  
4272  /**
4273   * Serialises this tool type
4274   *
4275   * @param stdClass $type The tool type
4276   *
4277   * @return array An array of values representing this type
4278   */
4279  function serialise_tool_type(stdClass $type) {
4280      global $CFG;
4281  
4282      $capabilitygroups = get_tool_type_capability_groups($type);
4283      $instanceids = get_tool_type_instance_ids($type);
4284      // Clean the name. We don't want tags here.
4285      $name = clean_param($type->name, PARAM_NOTAGS);
4286      if (!empty($type->description)) {
4287          // Clean the description. We don't want tags here.
4288          $description = clean_param($type->description, PARAM_NOTAGS);
4289      } else {
4290          $description = get_string('editdescription', 'mod_lti');
4291      }
4292      return array(
4293          'id' => $type->id,
4294          'name' => $name,
4295          'description' => $description,
4296          'urls' => get_tool_type_urls($type),
4297          'state' => get_tool_type_state_info($type),
4298          'platformid' => $CFG->wwwroot,
4299          'clientid' => $type->clientid,
4300          'deploymentid' => $type->id,
4301          'hascapabilitygroups' => !empty($capabilitygroups),
4302          'capabilitygroups' => $capabilitygroups,
4303          // Course ID of 1 means it's not linked to a course.
4304          'courseid' => $type->course == 1 ? 0 : $type->course,
4305          'instanceids' => $instanceids,
4306          'instancecount' => count($instanceids)
4307      );
4308  }
4309  
4310  /**
4311   * Loads the cartridge information into the tool type, if the launch url is for a cartridge file
4312   *
4313   * @param stdClass $type The tool type object to be filled in
4314   * @since Moodle 3.1
4315   */
4316  function lti_load_type_if_cartridge($type) {
4317      if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) {
4318          lti_load_type_from_cartridge($type->lti_toolurl, $type);
4319      }
4320  }
4321  
4322  /**
4323   * Loads the cartridge information into the new tool, if the launch url is for a cartridge file
4324   *
4325   * @param stdClass $lti The tools config
4326   * @since Moodle 3.1
4327   */
4328  function lti_load_tool_if_cartridge($lti) {
4329      if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) {
4330          lti_load_tool_from_cartridge($lti->toolurl, $lti);
4331      }
4332  }
4333  
4334  /**
4335   * Determines if the given url is for a IMS basic cartridge
4336   *
4337   * @param  string $url The url to be checked
4338   * @return True if the url is for a cartridge
4339   * @since Moodle 3.1
4340   */
4341  function lti_is_cartridge($url) {
4342      // If it is empty, it's not a cartridge.
4343      if (empty($url)) {
4344          return false;
4345      }
4346      // If it has xml at the end of the url, it's a cartridge.
4347      if (preg_match('/\.xml$/', $url)) {
4348          return true;
4349      }
4350      // Even if it doesn't have .xml, load the url to check if it's a cartridge..
4351      try {
4352          $toolinfo = lti_load_cartridge($url,
4353              array(
4354                  "launch_url" => "launchurl"
4355              )
4356          );
4357          if (!empty($toolinfo['launchurl'])) {
4358              return true;
4359          }
4360      } catch (moodle_exception $e) {
4361          return false; // Error loading the xml, so it's not a cartridge.
4362      }
4363      return false;
4364  }
4365  
4366  /**
4367   * Allows you to load settings for an external tool type from an IMS cartridge.
4368   *
4369   * @param  string   $url     The URL to the cartridge
4370   * @param  stdClass $type    The tool type object to be filled in
4371   * @throws moodle_exception if the cartridge could not be loaded correctly
4372   * @since Moodle 3.1
4373   */
4374  function lti_load_type_from_cartridge($url, $type) {
4375      $toolinfo = lti_load_cartridge($url,
4376          array(
4377              "title" => "lti_typename",
4378              "launch_url" => "lti_toolurl",
4379              "description" => "lti_description",
4380              "icon" => "lti_icon",
4381              "secure_icon" => "lti_secureicon"
4382          ),
4383          array(
4384              "icon_url" => "lti_extension_icon",
4385              "secure_icon_url" => "lti_extension_secureicon"
4386          )
4387      );
4388      // If an activity name exists, unset the cartridge name so we don't override it.
4389      if (isset($type->lti_typename)) {
4390          unset($toolinfo['lti_typename']);
4391      }
4392  
4393      // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4394      if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
4395          $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
4396      }
4397      unset($toolinfo['lti_extension_icon']);
4398  
4399      if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
4400          $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
4401      }
4402      unset($toolinfo['lti_extension_secureicon']);
4403  
4404      // Ensure Custom icons aren't overridden by cartridge params.
4405      if (!empty($type->lti_icon)) {
4406          unset($toolinfo['lti_icon']);
4407      }
4408  
4409      if (!empty($type->lti_secureicon)) {
4410          unset($toolinfo['lti_secureicon']);
4411      }
4412  
4413      foreach ($toolinfo as $property => $value) {
4414          $type->$property = $value;
4415      }
4416  }
4417  
4418  /**
4419   * Allows you to load in the configuration for an external tool from an IMS cartridge.
4420   *
4421   * @param  string   $url    The URL to the cartridge
4422   * @param  stdClass $lti    LTI object
4423   * @throws moodle_exception if the cartridge could not be loaded correctly
4424   * @since Moodle 3.1
4425   */
4426  function lti_load_tool_from_cartridge($url, $lti) {
4427      $toolinfo = lti_load_cartridge($url,
4428          array(
4429              "title" => "name",
4430              "launch_url" => "toolurl",
4431              "secure_launch_url" => "securetoolurl",
4432              "description" => "intro",
4433              "icon" => "icon",
4434              "secure_icon" => "secureicon"
4435          ),
4436          array(
4437              "icon_url" => "extension_icon",
4438              "secure_icon_url" => "extension_secureicon"
4439          )
4440      );
4441      // If an activity name exists, unset the cartridge name so we don't override it.
4442      if (isset($lti->name)) {
4443          unset($toolinfo['name']);
4444      }
4445  
4446      // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4447      if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
4448          $toolinfo['icon'] = $toolinfo['extension_icon'];
4449      }
4450      unset($toolinfo['extension_icon']);
4451  
4452      if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
4453          $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
4454      }
4455      unset($toolinfo['extension_secureicon']);
4456  
4457      foreach ($toolinfo as $property => $value) {
4458          $lti->$property = $value;
4459      }
4460  }
4461  
4462  /**
4463   * Search for a tag within an XML DOMDocument
4464   *
4465   * @param  string $url The url of the cartridge to be loaded
4466   * @param  array  $map The map of tags to keys in the return array
4467   * @param  array  $propertiesmap The map of properties to keys in the return array
4468   * @return array An associative array with the given keys and their values from the cartridge
4469   * @throws moodle_exception if the cartridge could not be loaded correctly
4470   * @since Moodle 3.1
4471   */
4472  function lti_load_cartridge($url, $map, $propertiesmap = array()) {
4473      global $CFG;
4474      require_once($CFG->libdir. "/filelib.php");
4475  
4476      $curl = new curl();
4477      $response = $curl->get($url);
4478  
4479      // Got a completely empty response (real or error), cannot process this with
4480      // DOMDocument::loadXML() because it errors with ValueError. So let's throw
4481      // the moodle_exception before waiting to examine the errors later.
4482      if (trim($response) === '') {
4483          throw new moodle_exception('errorreadingfile', '', '', $url);
4484      }
4485  
4486      // TODO MDL-46023 Replace this code with a call to the new library.
4487      $origerrors = libxml_use_internal_errors(true);
4488      libxml_clear_errors();
4489  
4490      $document = new DOMDocument();
4491      @$document->loadXML($response, LIBXML_NONET);
4492  
4493      $cartridge = new DomXpath($document);
4494  
4495      $errors = libxml_get_errors();
4496  
4497      libxml_clear_errors();
4498      libxml_use_internal_errors($origerrors);
4499  
4500      if (count($errors) > 0) {
4501          $message = 'Failed to load cartridge.';
4502          foreach ($errors as $error) {
4503              $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line;
4504          }
4505          throw new moodle_exception('errorreadingfile', '', '', $url, $message);
4506      }
4507  
4508      $toolinfo = array();
4509      foreach ($map as $tag => $key) {
4510          $value = get_tag($tag, $cartridge);
4511          if ($value) {
4512              $toolinfo[$key] = $value;
4513          }
4514      }
4515      if (!empty($propertiesmap)) {
4516          foreach ($propertiesmap as $property => $key) {
4517              $value = get_tag("property", $cartridge, $property);
4518              if ($value) {
4519                  $toolinfo[$key] = $value;
4520              }
4521          }
4522      }
4523  
4524      return $toolinfo;
4525  }
4526  
4527  /**
4528   * Search for a tag within an XML DOMDocument
4529   *
4530   * @param  stdClass $tagname The name of the tag to search for
4531   * @param  XPath    $xpath   The XML to find the tag in
4532   * @param  XPath    $attribute The attribute to search for (if we should search for a child node with the given
4533   * value for the name attribute
4534   * @since Moodle 3.1
4535   */
4536  function get_tag($tagname, $xpath, $attribute = null) {
4537      if ($attribute) {
4538          $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]');
4539      } else {
4540          $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']');
4541      }
4542      if ($result->length > 0) {
4543          return $result->item(0)->nodeValue;
4544      }
4545      return null;
4546  }
4547  
4548  /**
4549   * Create a new access token.
4550   *
4551   * @param int $typeid Tool type ID
4552   * @param string[] $scopes Scopes permitted for new token
4553   *
4554   * @return stdClass Access token
4555   */
4556  function lti_new_access_token($typeid, $scopes) {
4557      global $DB;
4558  
4559      // Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
4560      $numtries = 0;
4561      do {
4562          $numtries ++;
4563          $generatedtoken = md5(uniqid(rand(), 1));
4564          if ($numtries > 5) {
4565              throw new moodle_exception('Failed to generate LTI access token');
4566          }
4567      } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken)));
4568      $newtoken = new stdClass();
4569      $newtoken->typeid = $typeid;
4570      $newtoken->scope = json_encode(array_values($scopes));
4571      $newtoken->token = $generatedtoken;
4572  
4573      $newtoken->timecreated = time();
4574      $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE;
4575      $newtoken->lastaccess = null;
4576  
4577      $DB->insert_record('lti_access_tokens', $newtoken);
4578  
4579      return $newtoken;
4580  
4581  }
4582  
4583  
4584  /**
4585   * Wrapper for function libxml_disable_entity_loader() deprecated in PHP 8
4586   *
4587   * Method was deprecated in PHP 8 and it shows deprecation message. However it is still
4588   * required in the previous versions on PHP. While Moodle supports both PHP 7 and 8 we need to keep it.
4589   * @see https://php.watch/versions/8.0/libxml_disable_entity_loader-deprecation
4590   *
4591   * @param bool $value
4592   * @return bool
4593   *
4594   * @deprecated since Moodle 4.3
4595   */
4596  function lti_libxml_disable_entity_loader(bool $value): bool {
4597      debugging(__FUNCTION__ . '() is deprecated, please do not use it any more', DEBUG_DEVELOPER);
4598      return true;
4599  }