Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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      $config = new stdClass();
1427      $config->name = '';
1428      if (isset($item->title)) {
1429          $config->name = $item->title;
1430      }
1431      if (empty($config->name)) {
1432          $config->name = $tool->name;
1433      }
1434      if (isset($item->text)) {
1435          $config->introeditor = [
1436              'text' => $item->text,
1437              'format' => FORMAT_PLAIN
1438          ];
1439      } else {
1440          $config->introeditor = [
1441              'text' => '',
1442              'format' => FORMAT_PLAIN
1443          ];
1444      }
1445      if (isset($item->icon->{'@id'})) {
1446          $iconurl = new moodle_url($item->icon->{'@id'});
1447          // Assign item's icon URL to secureicon or icon depending on its scheme.
1448          if (strtolower($iconurl->get_scheme()) === 'https') {
1449              $config->secureicon = $iconurl->out(false);
1450          } else {
1451              $config->icon = $iconurl->out(false);
1452          }
1453      }
1454      if (isset($item->url)) {
1455          $url = new moodle_url($item->url);
1456          $config->toolurl = $url->out(false);
1457          $config->typeid = 0;
1458      } else {
1459          $config->typeid = $tool->id;
1460      }
1461      $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
1462      $islti2 = $tool->ltiversion === LTI_VERSION_2;
1463      if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
1464          $acceptgrades = $typeconfig->lti_acceptgrades;
1465          if ($acceptgrades == LTI_SETTING_ALWAYS) {
1466              // We create a line item regardless if the definition contains one or not.
1467              $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1468              $config->grade_modgrade_point = 100;
1469          }
1470          if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
1471              if (isset($item->lineItem)) {
1472                  $lineitem = $item->lineItem;
1473                  $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1474                  $maxscore = 100;
1475                  if (isset($lineitem->scoreConstraints)) {
1476                      $sc = $lineitem->scoreConstraints;
1477                      if (isset($sc->totalMaximum)) {
1478                          $maxscore = $sc->totalMaximum;
1479                      } else if (isset($sc->normalMaximum)) {
1480                          $maxscore = $sc->normalMaximum;
1481                      }
1482                  }
1483                  $config->grade_modgrade_point = $maxscore;
1484                  $config->lineitemresourceid = '';
1485                  $config->lineitemtag = '';
1486                  $config->lineitemsubreviewurl = '';
1487                  $config->lineitemsubreviewparams = '';
1488                  if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
1489                      $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';
1490                  }
1491                  if (isset($lineitem->tag)) {
1492                      $config->lineitemtag = $lineitem->tag?:'';
1493                  }
1494                  if (isset($lineitem->submissionReview)) {
1495                      $subreview = $lineitem->submissionReview;
1496                      $config->lineitemsubreviewurl = 'DEFAULT';
1497                      if (!empty($subreview->url)) {
1498                          $config->lineitemsubreviewurl = $subreview->url;
1499                      }
1500                      if (isset($subreview->custom)) {
1501                          $config->lineitemsubreviewparams = params_to_string($subreview->custom);
1502                      }
1503                  }
1504              }
1505          }
1506      }
1507      $config->instructorchoicesendname = LTI_SETTING_NEVER;
1508      $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
1509      $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
1510      if (isset($item->placementAdvice->presentationDocumentTarget)) {
1511          if ($item->placementAdvice->presentationDocumentTarget === 'window') {
1512              $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW;
1513          } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') {
1514              $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
1515          } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') {
1516              $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED;
1517          }
1518      }
1519      if (isset($item->custom)) {
1520          $config->instructorcustomparameters = params_to_string($item->custom);
1521      }
1522      return $config;
1523  }
1524  
1525  /**
1526   * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
1527   * selected content item. This configuration data can be then used when adding a tool into the course.
1528   *
1529   * @param int $typeid The tool type ID.
1530   * @param string $messagetype The value for the lti_message_type parameter.
1531   * @param string $ltiversion The value for the lti_version parameter.
1532   * @param string $consumerkey The consumer key.
1533   * @param string $contentitemsjson The JSON string for the content_items parameter.
1534   * @return stdClass The array of module information objects.
1535   * @throws moodle_exception
1536   * @throws lti\OAuthException
1537   */
1538  function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
1539      $tool = lti_get_type($typeid);
1540      // Validate parameters.
1541      if (!$tool) {
1542          throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1543      }
1544      // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
1545      // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
1546      if ($messagetype !== 'ContentItemSelection') {
1547          debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
1548              DEBUG_DEVELOPER);
1549      }
1550  
1551      // Check LTI versions from our side and the response's side. Show debugging if they don't match.
1552      // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
1553      $expectedversion = $tool->ltiversion;
1554      $islti2 = ($expectedversion === LTI_VERSION_2);
1555      if ($ltiversion !== $expectedversion) {
1556          debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
1557              " Response: {$ltiversion}", DEBUG_DEVELOPER);
1558      }
1559  
1560      $items = json_decode($contentitemsjson);
1561      if (empty($items)) {
1562          throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
1563      }
1564      if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {
1565          throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
1566      }
1567  
1568      $config = null;
1569      $items = $items->{'@graph'};
1570      if (!empty($items)) {
1571          $typeconfig = lti_get_type_type_config($tool->id);
1572          if (count($items) == 1) {
1573              $config = content_item_to_form($tool, $typeconfig, $items[0]);
1574          } else {
1575              $multiple = [];
1576              foreach ($items as $item) {
1577                  $multiple[] = content_item_to_form($tool, $typeconfig, $item);
1578              }
1579              $config = new stdClass();
1580              $config->multiple = $multiple;
1581          }
1582      }
1583      return $config;
1584  }
1585  
1586  /**
1587   * Converts the new Deep-Linking format for Content-Items to the old format.
1588   *
1589   * @param string $param JSON string representing new Deep-Linking format
1590   * @return string  JSON representation of content-items
1591   */
1592  function lti_convert_content_items($param) {
1593      $items = array();
1594      $json = json_decode($param);
1595      if (!empty($json) && is_array($json)) {
1596          foreach ($json as $item) {
1597              if (isset($item->type)) {
1598                  $newitem = clone $item;
1599                  switch ($item->type) {
1600                      case 'ltiResourceLink':
1601                          $newitem->{'@type'} = 'LtiLinkItem';
1602                          $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1603                          break;
1604                      case 'link':
1605                      case 'rich':
1606                          $newitem->{'@type'} = 'ContentItem';
1607                          $newitem->mediaType = 'text/html';
1608                          break;
1609                      case 'file':
1610                          $newitem->{'@type'} = 'FileItem';
1611                          break;
1612                  }
1613                  unset($newitem->type);
1614                  if (isset($item->html)) {
1615                      $newitem->text = $item->html;
1616                      unset($newitem->html);
1617                  }
1618                  if (isset($item->iframe)) {
1619                      // DeepLinking allows multiple options to be declared as supported.
1620                      // We favor iframe over new window if both are specified.
1621                      $newitem->placementAdvice = new stdClass();
1622                      $newitem->placementAdvice->presentationDocumentTarget = 'iframe';
1623                      if (isset($item->iframe->width)) {
1624                          $newitem->placementAdvice->displayWidth = $item->iframe->width;
1625                      }
1626                      if (isset($item->iframe->height)) {
1627                          $newitem->placementAdvice->displayHeight = $item->iframe->height;
1628                      }
1629                      unset($newitem->iframe);
1630                      unset($newitem->window);
1631                  } else if (isset($item->window)) {
1632                      $newitem->placementAdvice = new stdClass();
1633                      $newitem->placementAdvice->presentationDocumentTarget = 'window';
1634                      if (isset($item->window->targetName)) {
1635                          $newitem->placementAdvice->windowTarget = $item->window->targetName;
1636                      }
1637                      if (isset($item->window->width)) {
1638                          $newitem->placementAdvice->displayWidth = $item->window->width;
1639                      }
1640                      if (isset($item->window->height)) {
1641                          $newitem->placementAdvice->displayHeight = $item->window->height;
1642                      }
1643                      unset($newitem->window);
1644                  } else if (isset($item->presentation)) {
1645                      // This may have been part of an early draft but is not in the final spec
1646                      // so keeping it around for now in case it's actually been used.
1647                      $newitem->placementAdvice = new stdClass();
1648                      if (isset($item->presentation->documentTarget)) {
1649                          $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
1650                      }
1651                      if (isset($item->presentation->windowTarget)) {
1652                          $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget;
1653                      }
1654                      if (isset($item->presentation->width)) {
1655                          $newitem->placementAdvice->dislayWidth = $item->presentation->width;
1656                      }
1657                      if (isset($item->presentation->height)) {
1658                          $newitem->placementAdvice->dislayHeight = $item->presentation->height;
1659                      }
1660                      unset($newitem->presentation);
1661                  }
1662                  if (isset($item->icon) && isset($item->icon->url)) {
1663                      $newitem->icon->{'@id'} = $item->icon->url;
1664                      unset($newitem->icon->url);
1665                  }
1666                  if (isset($item->thumbnail) && isset($item->thumbnail->url)) {
1667                      $newitem->thumbnail->{'@id'} = $item->thumbnail->url;
1668                      unset($newitem->thumbnail->url);
1669                  }
1670                  if (isset($item->lineItem)) {
1671                      unset($newitem->lineItem);
1672                      $newitem->lineItem = new stdClass();
1673                      $newitem->lineItem->{'@type'} = 'LineItem';
1674                      $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore';
1675                      if (isset($item->lineItem->label)) {
1676                          $newitem->lineItem->label = $item->lineItem->label;
1677                      }
1678                      if (isset($item->lineItem->resourceId)) {
1679                          $newitem->lineItem->assignedActivity = new stdClass();
1680                          $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId;
1681                      }
1682                      if (isset($item->lineItem->tag)) {
1683                          $newitem->lineItem->tag = $item->lineItem->tag;
1684                      }
1685                      if (isset($item->lineItem->scoreMaximum)) {
1686                          $newitem->lineItem->scoreConstraints = new stdClass();
1687                          $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits';
1688                          $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum;
1689                      }
1690                      if (isset($item->lineItem->submissionReview)) {
1691                          $newitem->lineItem->submissionReview = $item->lineItem->submissionReview;
1692                      }
1693                  }
1694                  $items[] = $newitem;
1695              }
1696          }
1697      }
1698  
1699      $newitems = new stdClass();
1700      $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1701      $newitems->{'@graph'} = $items;
1702  
1703      return json_encode($newitems);
1704  }
1705  
1706  function lti_get_tool_table($tools, $id) {
1707      global $OUTPUT;
1708      $html = '';
1709  
1710      $typename = get_string('typename', 'lti');
1711      $baseurl = get_string('baseurl', 'lti');
1712      $action = get_string('action', 'lti');
1713      $createdon = get_string('createdon', 'lti');
1714  
1715      if (!empty($tools)) {
1716          $html .= "
1717          <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\">
1718              <table id=\"{$id}_tools\">
1719                  <thead>
1720                      <tr>
1721                          <th>$typename</th>
1722                          <th>$baseurl</th>
1723                          <th>$createdon</th>
1724                          <th>$action</th>
1725                      </tr>
1726                  </thead>
1727          ";
1728  
1729          foreach ($tools as $type) {
1730              $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1731              $accept = get_string('accept', 'lti');
1732              $update = get_string('update', 'lti');
1733              $delete = get_string('delete', 'lti');
1734  
1735              if (empty($type->toolproxyid)) {
1736                  $baseurl = new \moodle_url('/mod/lti/typessettings.php', array(
1737                          'action' => 'accept',
1738                          'id' => $type->id,
1739                          'sesskey' => sesskey(),
1740                          'tab' => $id
1741                      ));
1742                  $ref = $type->baseurl;
1743              } else {
1744                  $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array(
1745                          'action' => 'accept',
1746                          'id' => $type->id,
1747                          'sesskey' => sesskey(),
1748                          'tab' => $id
1749                      ));
1750                  $ref = $type->tpname;
1751              }
1752  
1753              $accepthtml = $OUTPUT->action_icon($baseurl,
1754                      new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1755                      array('title' => $accept, 'class' => 'editing_accept'));
1756  
1757              $deleteaction = 'delete';
1758  
1759              if ($type->state == LTI_TOOL_STATE_CONFIGURED) {
1760                  $accepthtml = '';
1761              }
1762  
1763              if ($type->state != LTI_TOOL_STATE_REJECTED) {
1764                  $deleteaction = 'reject';
1765                  $delete = get_string('reject', 'lti');
1766              }
1767  
1768              $updateurl = clone($baseurl);
1769              $updateurl->param('action', 'update');
1770              $updatehtml = $OUTPUT->action_icon($updateurl,
1771                      new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1772                      array('title' => $update, 'class' => 'editing_update'));
1773  
1774              if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) {
1775                  $deleteurl = clone($baseurl);
1776                  $deleteurl->param('action', $deleteaction);
1777                  $deletehtml = $OUTPUT->action_icon($deleteurl,
1778                          new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1779                          array('title' => $delete, 'class' => 'editing_delete'));
1780              } else {
1781                  $deletehtml = '';
1782              }
1783              $html .= "
1784              <tr>
1785                  <td>
1786                      {$type->name}
1787                  </td>
1788                  <td>
1789                      {$ref}
1790                  </td>
1791                  <td>
1792                      {$date}
1793                  </td>
1794                  <td align=\"center\">
1795                      {$accepthtml}{$updatehtml}{$deletehtml}
1796                  </td>
1797              </tr>
1798              ";
1799          }
1800          $html .= '</table></div>';
1801      } else {
1802          $html .= get_string('no_' . $id, 'lti');
1803      }
1804  
1805      return $html;
1806  }
1807  
1808  /**
1809   * This function builds the tab for a category of tool proxies
1810   *
1811   * @param object    $toolproxies    Tool proxy instance objects
1812   * @param string    $id             Category ID
1813   *
1814   * @return string                   HTML for tab
1815   */
1816  function lti_get_tool_proxy_table($toolproxies, $id) {
1817      global $OUTPUT;
1818  
1819      if (!empty($toolproxies)) {
1820          $typename = get_string('typename', 'lti');
1821          $url = get_string('registrationurl', 'lti');
1822          $action = get_string('action', 'lti');
1823          $createdon = get_string('createdon', 'lti');
1824  
1825          $html = <<< EOD
1826          <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em">
1827              <table id="{$id}_tool_proxies">
1828                  <thead>
1829                      <tr>
1830                          <th>{$typename}</th>
1831                          <th>{$url}</th>
1832                          <th>{$createdon}</th>
1833                          <th>{$action}</th>
1834                      </tr>
1835                  </thead>
1836  EOD;
1837          foreach ($toolproxies as $toolproxy) {
1838              $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1839              $accept = get_string('register', 'lti');
1840              $update = get_string('update', 'lti');
1841              $delete = get_string('delete', 'lti');
1842  
1843              $baseurl = new \moodle_url('/mod/lti/registersettings.php', array(
1844                      'action' => 'accept',
1845                      'id' => $toolproxy->id,
1846                      'sesskey' => sesskey(),
1847                      'tab' => $id
1848                  ));
1849  
1850              $registerurl = new \moodle_url('/mod/lti/register.php', array(
1851                      'id' => $toolproxy->id,
1852                      'sesskey' => sesskey(),
1853                      'tab' => 'tool_proxy'
1854                  ));
1855  
1856              $accepthtml = $OUTPUT->action_icon($registerurl,
1857                      new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1858                      array('title' => $accept, 'class' => 'editing_accept'));
1859  
1860              $deleteaction = 'delete';
1861  
1862              if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) {
1863                  $accepthtml = '';
1864              }
1865  
1866              if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) {
1867                  $delete = get_string('cancel', 'lti');
1868              }
1869  
1870              $updateurl = clone($baseurl);
1871              $updateurl->param('action', 'update');
1872              $updatehtml = $OUTPUT->action_icon($updateurl,
1873                      new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1874                      array('title' => $update, 'class' => 'editing_update'));
1875  
1876              $deleteurl = clone($baseurl);
1877              $deleteurl->param('action', $deleteaction);
1878              $deletehtml = $OUTPUT->action_icon($deleteurl,
1879                      new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1880                      array('title' => $delete, 'class' => 'editing_delete'));
1881              $html .= <<< EOD
1882              <tr>
1883                  <td>
1884                      {$toolproxy->name}
1885                  </td>
1886                  <td>
1887                      {$toolproxy->regurl}
1888                  </td>
1889                  <td>
1890                      {$date}
1891                  </td>
1892                  <td align="center">
1893                      {$accepthtml}{$updatehtml}{$deletehtml}
1894                  </td>
1895              </tr>
1896  EOD;
1897          }
1898          $html .= '</table></div>';
1899      } else {
1900          $html = get_string('no_' . $id, 'lti');
1901      }
1902  
1903      return $html;
1904  }
1905  
1906  /**
1907   * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter
1908   *
1909   * @param object $tool  Tool instance object
1910   *
1911   * @return array List of enabled capabilities
1912   */
1913  function lti_get_enabled_capabilities($tool) {
1914      if (!isset($tool)) {
1915          return array();
1916      }
1917      if (!empty($tool->enabledcapability)) {
1918          $enabledcapabilities = explode("\n", $tool->enabledcapability);
1919      } else {
1920          $enabledcapabilities = array();
1921      }
1922      if (!empty($tool->parameter)) {
1923          $paramstr = str_replace("\r\n", "\n", $tool->parameter);
1924          $paramstr = str_replace("\n\r", "\n", $paramstr);
1925          $paramstr = str_replace("\r", "\n", $paramstr);
1926          $params = explode("\n", $paramstr);
1927          foreach ($params as $param) {
1928              $pos = strpos($param, '=');
1929              if (($pos === false) || ($pos < 1)) {
1930                  continue;
1931              }
1932              $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
1933              if (substr($value, 0, 1) == '$') {
1934                  $value = substr($value, 1);
1935                  if (!in_array($value, $enabledcapabilities)) {
1936                      $enabledcapabilities[] = $value;
1937                  }
1938              }
1939          }
1940      }
1941      return $enabledcapabilities;
1942  }
1943  
1944  /**
1945   * Splits the custom parameters
1946   *
1947   * @param string    $customstr      String containing the parameters
1948   *
1949   * @return array of custom parameters
1950   */
1951  function lti_split_parameters($customstr) {
1952      $customstr = str_replace("\r\n", "\n", $customstr);
1953      $customstr = str_replace("\n\r", "\n", $customstr);
1954      $customstr = str_replace("\r", "\n", $customstr);
1955      $lines = explode("\n", $customstr);  // Or should this split on "/[\n;]/"?
1956      $retval = array();
1957      foreach ($lines as $line) {
1958          $pos = strpos($line, '=');
1959          if ( $pos === false || $pos < 1 ) {
1960              continue;
1961          }
1962          $key = trim(core_text::substr($line, 0, $pos));
1963          $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
1964          $retval[$key] = $val;
1965      }
1966      return $retval;
1967  }
1968  
1969  /**
1970   * Splits the custom parameters field to the various parameters
1971   *
1972   * @param object    $toolproxy      Tool proxy instance object
1973   * @param object    $tool           Tool instance object
1974   * @param array     $params         LTI launch parameters
1975   * @param string    $customstr      String containing the parameters
1976   * @param boolean   $islti2         True if an LTI 2 tool is being launched
1977   *
1978   * @return array of custom parameters
1979   */
1980  function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {
1981      $splitted = lti_split_parameters($customstr);
1982      $retval = array();
1983      foreach ($splitted as $key => $val) {
1984          $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
1985          $key2 = lti_map_keyname($key);
1986          $retval['custom_'.$key2] = $val;
1987          if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) {
1988              $retval['custom_'.$key] = $val;
1989          }
1990      }
1991      return $retval;
1992  }
1993  
1994  /**
1995   * Adds the custom parameters to an array
1996   *
1997   * @param object    $toolproxy      Tool proxy instance object
1998   * @param object    $tool           Tool instance object
1999   * @param array     $params         LTI launch parameters
2000   * @param array     $parameters     Array containing the parameters
2001   *
2002   * @return array    Array of custom parameters
2003   */
2004  function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {
2005      $retval = array();
2006      foreach ($parameters as $key => $val) {
2007          $key2 = lti_map_keyname($key);
2008          $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true);
2009          $retval['custom_'.$key2] = $val;
2010          if ($key != $key2) {
2011              $retval['custom_'.$key] = $val;
2012          }
2013      }
2014      return $retval;
2015  }
2016  
2017  /**
2018   * Parse a custom parameter to replace any substitution variables
2019   *
2020   * @param object    $toolproxy      Tool proxy instance object
2021   * @param object    $tool           Tool instance object
2022   * @param array     $params         LTI launch parameters
2023   * @param string    $value          Custom parameter value
2024   * @param boolean   $islti2         True if an LTI 2 tool is being launched
2025   *
2026   * @return string Parsed value of custom parameter
2027   */
2028  function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {
2029      // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var.
2030      global $USER, $COURSE;
2031  
2032      if ($value) {
2033          if (substr($value, 0, 1) == '\\') {
2034              $value = substr($value, 1);
2035          } else if (substr($value, 0, 1) == '$') {
2036              $value1 = substr($value, 1);
2037              $enabledcapabilities = lti_get_enabled_capabilities($tool);
2038              if (!$islti2 || in_array($value1, $enabledcapabilities)) {
2039                  $capabilities = lti_get_capabilities();
2040                  if (array_key_exists($value1, $capabilities)) {
2041                      $val = $capabilities[$value1];
2042                      if ($val) {
2043                          if (substr($val, 0, 1) != '$') {
2044                              $value = $params[$val];
2045                          } else {
2046                              $valarr = explode('->', substr($val, 1), 2);
2047                              $value = "{${$valarr[0]}->{$valarr[1]}}";
2048                              $value = str_replace('<br />' , ' ', $value);
2049                              $value = str_replace('<br>' , ' ', $value);
2050                              $value = format_string($value);
2051                          }
2052                      } else {
2053                          $value = lti_calculate_custom_parameter($value1);
2054                      }
2055                  } else {
2056                      $val = $value;
2057                      $services = lti_get_services();
2058                      foreach ($services as $service) {
2059                          $service->set_tool_proxy($toolproxy);
2060                          $service->set_type($tool);
2061                          $value = $service->parse_value($val);
2062                          if ($val != $value) {
2063                              break;
2064                          }
2065                      }
2066                  }
2067              }
2068          }
2069      }
2070      return $value;
2071  }
2072  
2073  /**
2074   * Calculates the value of a custom parameter that has not been specified earlier
2075   *
2076   * @param string    $value          Custom parameter value
2077   *
2078   * @return string Calculated value of custom parameter
2079   */
2080  function lti_calculate_custom_parameter($value) {
2081      global $USER, $COURSE;
2082  
2083      switch ($value) {
2084          case 'Moodle.Person.userGroupIds':
2085              return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]);
2086          case 'Context.id.history':
2087              return implode(",", get_course_history($COURSE));
2088          case 'CourseSection.timeFrame.begin':
2089              if (empty($COURSE->startdate)) {
2090                  return "";
2091              }
2092              $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC'));
2093              return $dt->format(DateTime::ATOM);
2094          case 'CourseSection.timeFrame.end':
2095              if (empty($COURSE->enddate)) {
2096                  return "";
2097              }
2098              $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC'));
2099              return $dt->format(DateTime::ATOM);
2100      }
2101      return null;
2102  }
2103  
2104  /**
2105   * Build the history chain for this course using the course originalcourseid.
2106   *
2107   * @param object $course course for which the history is returned.
2108   *
2109   * @return array ids of the source course in ancestry order, immediate parent 1st.
2110   */
2111  function get_course_history($course) {
2112      global $DB;
2113      $history = [];
2114      $parentid = $course->originalcourseid;
2115      while (!empty($parentid) && !in_array($parentid, $history)) {
2116          $history[] = $parentid;
2117          $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid));
2118      }
2119      return $history;
2120  }
2121  
2122  /**
2123   * Used for building the names of the different custom parameters
2124   *
2125   * @param string $key   Parameter name
2126   * @param bool $tolower Do we want to convert the key into lower case?
2127   * @return string       Processed name
2128   */
2129  function lti_map_keyname($key, $tolower = true) {
2130      if ($tolower) {
2131          $newkey = '';
2132          $key = core_text::strtolower(trim($key));
2133          foreach (str_split($key) as $ch) {
2134              if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {
2135                  $newkey .= $ch;
2136              } else {
2137                  $newkey .= '_';
2138              }
2139          }
2140      } else {
2141          $newkey = $key;
2142      }
2143      return $newkey;
2144  }
2145  
2146  /**
2147   * Gets the IMS role string for the specified user and LTI course module.
2148   *
2149   * @param mixed    $user      User object or user id
2150   * @param int      $cmid      The course module id of the LTI activity
2151   * @param int      $courseid  The course id of the LTI activity
2152   * @param boolean  $islti2    True if an LTI 2 tool is being launched
2153   *
2154   * @return string A role string suitable for passing with an LTI launch
2155   */
2156  function lti_get_ims_role($user, $cmid, $courseid, $islti2) {
2157      $roles = array();
2158  
2159      if (empty($cmid)) {
2160          // If no cmid is passed, check if the user is a teacher in the course
2161          // This allows other modules to programmatically "fake" a launch without
2162          // a real LTI instance.
2163          $context = context_course::instance($courseid);
2164  
2165          if (has_capability('moodle/course:manageactivities', $context, $user)) {
2166              array_push($roles, 'Instructor');
2167          } else {
2168              array_push($roles, 'Learner');
2169          }
2170      } else {
2171          $context = context_module::instance($cmid);
2172  
2173          if (has_capability('mod/lti:manage', $context)) {
2174              array_push($roles, 'Instructor');
2175          } else {
2176              array_push($roles, 'Learner');
2177          }
2178      }
2179  
2180      if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) {
2181          // Make sure admins do not have the Learner role, then set admin role.
2182          $roles = array_diff($roles, array('Learner'));
2183          if (!$islti2) {
2184              array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
2185          } else {
2186              array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator');
2187          }
2188      }
2189  
2190      return join(',', $roles);
2191  }
2192  
2193  /**
2194   * Returns configuration details for the tool
2195   *
2196   * @param int $typeid   Basic LTI tool typeid
2197   *
2198   * @return array        Tool Configuration
2199   */
2200  function lti_get_type_config($typeid) {
2201      global $DB;
2202  
2203      $query = "SELECT name, value
2204                  FROM {lti_types_config}
2205                 WHERE typeid = :typeid1
2206             UNION ALL
2207                SELECT 'toolurl' AS name, baseurl AS value
2208                  FROM {lti_types}
2209                 WHERE id = :typeid2
2210             UNION ALL
2211                SELECT 'icon' AS name, icon AS value
2212                  FROM {lti_types}
2213                 WHERE id = :typeid3
2214             UNION ALL
2215                SELECT 'secureicon' AS name, secureicon AS value
2216                  FROM {lti_types}
2217                 WHERE id = :typeid4";
2218  
2219      $typeconfig = array();
2220      $configs = $DB->get_records_sql($query,
2221          array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid));
2222  
2223      if (!empty($configs)) {
2224          foreach ($configs as $config) {
2225              $typeconfig[$config->name] = $config->value;
2226          }
2227      }
2228  
2229      return $typeconfig;
2230  }
2231  
2232  function lti_get_tools_by_url($url, $state, $courseid = null) {
2233      $domain = lti_get_domain_from_url($url);
2234  
2235      return lti_get_tools_by_domain($domain, $state, $courseid);
2236  }
2237  
2238  function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
2239      global $DB, $SITE;
2240  
2241      $statefilter = '';
2242      $coursefilter = '';
2243  
2244      if ($state) {
2245          $statefilter = 'AND state = :state';
2246      }
2247  
2248      if ($courseid && $courseid != $SITE->id) {
2249          $coursefilter = 'OR course = :courseid';
2250      }
2251  
2252      $query = "SELECT *
2253                  FROM {lti_types}
2254                 WHERE tooldomain = :tooldomain
2255                   AND (course = :siteid $coursefilter)
2256                   $statefilter";
2257  
2258      return $DB->get_records_sql($query, array(
2259          'courseid' => $courseid,
2260          'siteid' => $SITE->id,
2261          'tooldomain' => $domain,
2262          'state' => $state
2263      ));
2264  }
2265  
2266  /**
2267   * Returns all basicLTI tools configured by the administrator
2268   *
2269   * @param int $course
2270   *
2271   * @return array
2272   */
2273  function lti_filter_get_types($course) {
2274      global $DB;
2275  
2276      if (!empty($course)) {
2277          $where = "WHERE t.course = :course";
2278          $params = array('course' => $course);
2279      } else {
2280          $where = '';
2281          $params = array();
2282      }
2283      $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname
2284                  FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id
2285                  {$where}";
2286      return $DB->get_records_sql($query, $params);
2287  }
2288  
2289  /**
2290   * Given an array of tools, filter them based on their state
2291   *
2292   * @param array $tools An array of lti_types records
2293   * @param int $state One of the LTI_TOOL_STATE_* constants
2294   * @return array
2295   */
2296  function lti_filter_tool_types(array $tools, $state) {
2297      $return = array();
2298      foreach ($tools as $key => $tool) {
2299          if ($tool->state == $state) {
2300              $return[$key] = $tool;
2301          }
2302      }
2303      return $return;
2304  }
2305  
2306  /**
2307   * Returns all lti types visible in this course
2308   *
2309   * @param int $courseid The id of the course to retieve types for
2310   * @param array $coursevisible options for 'coursevisible' field,
2311   *        default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]
2312   * @return stdClass[] All the lti types visible in the given course
2313   */
2314  function lti_get_lti_types_by_course($courseid, $coursevisible = null) {
2315      global $DB, $SITE;
2316  
2317      if ($coursevisible === null) {
2318          $coursevisible = [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER];
2319      }
2320  
2321      list($coursevisiblesql, $coursevisparams) = $DB->get_in_or_equal($coursevisible, SQL_PARAMS_NAMED, 'coursevisible');
2322      $courseconds = [];
2323      if (has_capability('mod/lti:addmanualinstance', context_course::instance($courseid))) {
2324          $courseconds[] = "course = :courseid";
2325      }
2326      if (has_capability('mod/lti:addpreconfiguredinstance', context_course::instance($courseid))) {
2327          $courseconds[] = "course = :siteid";
2328      }
2329      if (!$courseconds) {
2330          return [];
2331      }
2332      $coursecond = implode(" OR ", $courseconds);
2333      $query = "SELECT *
2334                  FROM {lti_types}
2335                 WHERE coursevisible $coursevisiblesql
2336                   AND ($coursecond)
2337                   AND state = :active
2338              ORDER BY name ASC";
2339  
2340      return $DB->get_records_sql($query,
2341          array('siteid' => $SITE->id, 'courseid' => $courseid, 'active' => LTI_TOOL_STATE_CONFIGURED) + $coursevisparams);
2342  }
2343  
2344  /**
2345   * Returns tool types for lti add instance and edit page
2346   *
2347   * @return array Array of lti types
2348   */
2349  function lti_get_types_for_add_instance() {
2350      global $COURSE;
2351      $admintypes = lti_get_lti_types_by_course($COURSE->id);
2352  
2353      $types = array();
2354      if (has_capability('mod/lti:addmanualinstance', context_course::instance($COURSE->id))) {
2355          $types[0] = (object)array('name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null);
2356      }
2357  
2358      foreach ($admintypes as $type) {
2359          $types[$type->id] = $type;
2360      }
2361  
2362      return $types;
2363  }
2364  
2365  /**
2366   * Returns a list of configured types in the given course
2367   *
2368   * @param int $courseid The id of the course to retieve types for
2369   * @param int $sectionreturn section to return to for forming the URLs
2370   * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link
2371   */
2372  function lti_get_configured_types($courseid, $sectionreturn = 0) {
2373      global $OUTPUT;
2374      $types = array();
2375      $admintypes = lti_get_lti_types_by_course($courseid, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]);
2376  
2377      foreach ($admintypes as $ltitype) {
2378          $type           = new stdClass();
2379          $type->id       = $ltitype->id;
2380          $type->modclass = MOD_CLASS_ACTIVITY;
2381          $type->name     = 'lti_type_' . $ltitype->id;
2382          // Clean the name. We don't want tags here.
2383          $type->title    = clean_param($ltitype->name, PARAM_NOTAGS);
2384          $trimmeddescription = trim($ltitype->description ?? '');
2385          if ($trimmeddescription != '') {
2386              // Clean the description. We don't want tags here.
2387              $type->help     = clean_param($trimmeddescription, PARAM_NOTAGS);
2388              $type->helplink = get_string('modulename_shortcut_link', 'lti');
2389          }
2390  
2391          $iconurl = get_tool_type_icon_url($ltitype);
2392          $iconclass = '';
2393          if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) {
2394              // Do not filter the icon if it is not the default LTI activity icon.
2395              $iconclass = 'nofilter';
2396          }
2397          $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]);
2398  
2399          $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'course' => $courseid,
2400              'sr' => $sectionreturn, 'typeid' => $ltitype->id));
2401          $types[] = $type;
2402      }
2403      return $types;
2404  }
2405  
2406  function lti_get_domain_from_url($url) {
2407      $matches = array();
2408  
2409      if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) {
2410          return $matches[1];
2411      }
2412  }
2413  
2414  function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) {
2415      $possibletools = lti_get_tools_by_url($url, $state, $courseid);
2416  
2417      return lti_get_best_tool_by_url($url, $possibletools, $courseid);
2418  }
2419  
2420  function lti_get_url_thumbprint($url) {
2421      // Parse URL requires a schema otherwise everything goes into 'path'.  Fixed 5.4.7 or later.
2422      if (preg_match('/https?:\/\//', $url) !== 1) {
2423          $url = 'http://'.$url;
2424      }
2425      $urlparts = parse_url(strtolower($url));
2426      if (!isset($urlparts['path'])) {
2427          $urlparts['path'] = '';
2428      }
2429  
2430      if (!isset($urlparts['query'])) {
2431          $urlparts['query'] = '';
2432      }
2433  
2434      if (!isset($urlparts['host'])) {
2435          $urlparts['host'] = '';
2436      }
2437  
2438      if (substr($urlparts['host'], 0, 4) === 'www.') {
2439          $urlparts['host'] = substr($urlparts['host'], 4);
2440      }
2441  
2442      $urllower = $urlparts['host'] . '/' . $urlparts['path'];
2443  
2444      if ($urlparts['query'] != '') {
2445          $urllower .= '?' . $urlparts['query'];
2446      }
2447  
2448      return $urllower;
2449  }
2450  
2451  function lti_get_best_tool_by_url($url, $tools, $courseid = null) {
2452      if (count($tools) === 0) {
2453          return null;
2454      }
2455  
2456      $urllower = lti_get_url_thumbprint($url);
2457  
2458      foreach ($tools as $tool) {
2459          $tool->_matchscore = 0;
2460  
2461          $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl);
2462  
2463          if ($urllower === $toolbaseurllower) {
2464              // 100 points for exact thumbprint match.
2465              $tool->_matchscore += 100;
2466          } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) {
2467              // 50 points if tool thumbprint starts with the base URL thumbprint.
2468              $tool->_matchscore += 50;
2469          }
2470  
2471          // Prefer course tools over site tools.
2472          if (!empty($courseid)) {
2473              // Minus 10 points for not matching the course id (global tools).
2474              if ($tool->course != $courseid) {
2475                  $tool->_matchscore -= 10;
2476              }
2477          }
2478      }
2479  
2480      $bestmatch = array_reduce($tools, function($value, $tool) {
2481          if ($tool->_matchscore > $value->_matchscore) {
2482              return $tool;
2483          } else {
2484              return $value;
2485          }
2486  
2487      }, (object)array('_matchscore' => -1));
2488  
2489      // None of the tools are suitable for this URL.
2490      if ($bestmatch->_matchscore <= 0) {
2491          return null;
2492      }
2493  
2494      return $bestmatch;
2495  }
2496  
2497  function lti_get_shared_secrets_by_key($key) {
2498      global $DB;
2499  
2500      // Look up the shared secret for the specified key in both the types_config table (for configured tools)
2501      // And in the lti resource table for ad-hoc tools.
2502      $lti13 = LTI_VERSION_1P3;
2503      $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value
2504                  FROM {lti_types_config} t1
2505                  JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
2506                  JOIN {lti_types} type ON t2.typeid = type.id
2507                WHERE t1.name = 'resourcekey'
2508                  AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1
2509                  AND t2.name = 'password'
2510                  AND type.state = :configured1
2511                  AND type.ltiversion <> :ltiversion
2512                 UNION
2513                SELECT tp.secret AS value
2514                  FROM {lti_tool_proxies} tp
2515                  JOIN {lti_types} t ON tp.id = t.toolproxyid
2516                WHERE tp.guid = :key2
2517                  AND t.state = :configured2
2518                 UNION
2519                SELECT password AS value
2520                 FROM {lti}
2521                WHERE resourcekey = :key3";
2522  
2523      $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13,
2524          'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));
2525  
2526      $values = array_map(function($item) {
2527          return $item->value;
2528      }, $sharedsecrets);
2529  
2530      // There should really only be one shared secret per key. But, we can't prevent
2531      // more than one getting entered. For instance, if the same key is used for two tool providers.
2532      return $values;
2533  }
2534  
2535  /**
2536   * Delete a Basic LTI configuration
2537   *
2538   * @param int $id   Configuration id
2539   */
2540  function lti_delete_type($id) {
2541      global $DB;
2542  
2543      // We should probably just copy the launch URL to the tool instances in this case... using a single query.
2544      /*
2545      $instances = $DB->get_records('lti', array('typeid' => $id));
2546      foreach ($instances as $instance) {
2547          $instance->typeid = 0;
2548          $DB->update_record('lti', $instance);
2549      }*/
2550  
2551      $DB->delete_records('lti_types', array('id' => $id));
2552      $DB->delete_records('lti_types_config', array('typeid' => $id));
2553  }
2554  
2555  function lti_set_state_for_type($id, $state) {
2556      global $DB;
2557  
2558      $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));
2559  }
2560  
2561  /**
2562   * Transforms a basic LTI object to an array
2563   *
2564   * @param object $ltiobject    Basic LTI object
2565   *
2566   * @return array Basic LTI configuration details
2567   */
2568  function lti_get_config($ltiobject) {
2569      $typeconfig = (array)$ltiobject;
2570      $additionalconfig = lti_get_type_config($ltiobject->typeid);
2571      $typeconfig = array_merge($typeconfig, $additionalconfig);
2572      return $typeconfig;
2573  }
2574  
2575  /**
2576   *
2577   * Generates some of the tool configuration based on the instance details
2578   *
2579   * @param int $id
2580   *
2581   * @return object configuration
2582   *
2583   */
2584  function lti_get_type_config_from_instance($id) {
2585      global $DB;
2586  
2587      $instance = $DB->get_record('lti', array('id' => $id));
2588      $config = lti_get_config($instance);
2589  
2590      $type = new \stdClass();
2591      $type->lti_fix = $id;
2592      if (isset($config['toolurl'])) {
2593          $type->lti_toolurl = $config['toolurl'];
2594      }
2595      if (isset($config['instructorchoicesendname'])) {
2596          $type->lti_sendname = $config['instructorchoicesendname'];
2597      }
2598      if (isset($config['instructorchoicesendemailaddr'])) {
2599          $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr'];
2600      }
2601      if (isset($config['instructorchoiceacceptgrades'])) {
2602          $type->lti_acceptgrades = $config['instructorchoiceacceptgrades'];
2603      }
2604      if (isset($config['instructorchoiceallowroster'])) {
2605          $type->lti_allowroster = $config['instructorchoiceallowroster'];
2606      }
2607  
2608      if (isset($config['instructorcustomparameters'])) {
2609          $type->lti_allowsetting = $config['instructorcustomparameters'];
2610      }
2611      return $type;
2612  }
2613  
2614  /**
2615   * Generates some of the tool configuration based on the admin configuration details
2616   *
2617   * @param int $id
2618   *
2619   * @return stdClass Configuration details
2620   */
2621  function lti_get_type_type_config($id) {
2622      global $DB;
2623  
2624      $basicltitype = $DB->get_record('lti_types', array('id' => $id));
2625      $config = lti_get_type_config($id);
2626  
2627      $type = new \stdClass();
2628  
2629      $type->lti_typename = $basicltitype->name;
2630  
2631      $type->typeid = $basicltitype->id;
2632  
2633      $type->toolproxyid = $basicltitype->toolproxyid;
2634  
2635      $type->lti_toolurl = $basicltitype->baseurl;
2636  
2637      $type->lti_ltiversion = $basicltitype->ltiversion;
2638  
2639      $type->lti_clientid = $basicltitype->clientid;
2640      $type->lti_clientid_disabled = $type->lti_clientid;
2641  
2642      $type->lti_description = $basicltitype->description;
2643  
2644      $type->lti_parameters = $basicltitype->parameter;
2645  
2646      $type->lti_icon = $basicltitype->icon;
2647  
2648      $type->lti_secureicon = $basicltitype->secureicon;
2649  
2650      if (isset($config['resourcekey'])) {
2651          $type->lti_resourcekey = $config['resourcekey'];
2652      }
2653      if (isset($config['password'])) {
2654          $type->lti_password = $config['password'];
2655      }
2656      if (isset($config['publickey'])) {
2657          $type->lti_publickey = $config['publickey'];
2658      }
2659      if (isset($config['publickeyset'])) {
2660          $type->lti_publickeyset = $config['publickeyset'];
2661      }
2662      if (isset($config['keytype'])) {
2663          $type->lti_keytype = $config['keytype'];
2664      }
2665      if (isset($config['initiatelogin'])) {
2666          $type->lti_initiatelogin = $config['initiatelogin'];
2667      }
2668      if (isset($config['redirectionuris'])) {
2669          $type->lti_redirectionuris = $config['redirectionuris'];
2670      }
2671  
2672      if (isset($config['sendname'])) {
2673          $type->lti_sendname = $config['sendname'];
2674      }
2675      if (isset($config['instructorchoicesendname'])) {
2676          $type->lti_instructorchoicesendname = $config['instructorchoicesendname'];
2677      }
2678      if (isset($config['sendemailaddr'])) {
2679          $type->lti_sendemailaddr = $config['sendemailaddr'];
2680      }
2681      if (isset($config['instructorchoicesendemailaddr'])) {
2682          $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr'];
2683      }
2684      if (isset($config['acceptgrades'])) {
2685          $type->lti_acceptgrades = $config['acceptgrades'];
2686      }
2687      if (isset($config['instructorchoiceacceptgrades'])) {
2688          $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades'];
2689      }
2690      if (isset($config['allowroster'])) {
2691          $type->lti_allowroster = $config['allowroster'];
2692      }
2693      if (isset($config['instructorchoiceallowroster'])) {
2694          $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster'];
2695      }
2696  
2697      if (isset($config['customparameters'])) {
2698          $type->lti_customparameters = $config['customparameters'];
2699      }
2700  
2701      if (isset($config['forcessl'])) {
2702          $type->lti_forcessl = $config['forcessl'];
2703      }
2704  
2705      if (isset($config['organizationid_default'])) {
2706          $type->lti_organizationid_default = $config['organizationid_default'];
2707      } else {
2708          // Tool was configured before this option was available and the default then was host.
2709          $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
2710      }
2711      if (isset($config['organizationid'])) {
2712          $type->lti_organizationid = $config['organizationid'];
2713      }
2714      if (isset($config['organizationurl'])) {
2715          $type->lti_organizationurl = $config['organizationurl'];
2716      }
2717      if (isset($config['organizationdescr'])) {
2718          $type->lti_organizationdescr = $config['organizationdescr'];
2719      }
2720      if (isset($config['launchcontainer'])) {
2721          $type->lti_launchcontainer = $config['launchcontainer'];
2722      }
2723  
2724      if (isset($config['coursevisible'])) {
2725          $type->lti_coursevisible = $config['coursevisible'];
2726      }
2727  
2728      if (isset($config['contentitem'])) {
2729          $type->lti_contentitem = $config['contentitem'];
2730      }
2731  
2732      if (isset($config['toolurl_ContentItemSelectionRequest'])) {
2733          $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];
2734      }
2735  
2736      if (isset($config['debuglaunch'])) {
2737          $type->lti_debuglaunch = $config['debuglaunch'];
2738      }
2739  
2740      if (isset($config['module_class_type'])) {
2741          $type->lti_module_class_type = $config['module_class_type'];
2742      }
2743  
2744      // Get the parameters from the LTI services.
2745      foreach ($config as $name => $value) {
2746          if (strpos($name, 'ltiservice_') === 0) {
2747              $type->{$name} = $config[$name];
2748          }
2749      }
2750  
2751      return $type;
2752  }
2753  
2754  function lti_prepare_type_for_save($type, $config) {
2755      if (isset($config->lti_toolurl)) {
2756          $type->baseurl = $config->lti_toolurl;
2757          if (isset($config->lti_tooldomain)) {
2758              $type->tooldomain = $config->lti_tooldomain;
2759          } else {
2760              $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
2761          }
2762      }
2763      if (isset($config->lti_description)) {
2764          $type->description = $config->lti_description;
2765      }
2766      if (isset($config->lti_typename)) {
2767          $type->name = $config->lti_typename;
2768      }
2769      if (isset($config->lti_ltiversion)) {
2770          $type->ltiversion = $config->lti_ltiversion;
2771      }
2772      if (isset($config->lti_clientid)) {
2773          $type->clientid = $config->lti_clientid;
2774      }
2775      if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {
2776          $type->clientid = registration_helper::get()->new_clientid();
2777      } else if (empty($type->clientid)) {
2778          $type->clientid = null;
2779      }
2780      if (isset($config->lti_coursevisible)) {
2781          $type->coursevisible = $config->lti_coursevisible;
2782      }
2783  
2784      if (isset($config->lti_icon)) {
2785          $type->icon = $config->lti_icon;
2786      }
2787      if (isset($config->lti_secureicon)) {
2788          $type->secureicon = $config->lti_secureicon;
2789      }
2790  
2791      $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0;
2792      $config->lti_forcessl = $type->forcessl;
2793      if (isset($config->lti_contentitem)) {
2794          $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;
2795          $config->lti_contentitem = $type->contentitem;
2796      }
2797      if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
2798          if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {
2799              $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;
2800          } else {
2801              $type->toolurl_ContentItemSelectionRequest = '';
2802          }
2803          $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;
2804      }
2805  
2806      $type->timemodified = time();
2807  
2808      unset ($config->lti_typename);
2809      unset ($config->lti_toolurl);
2810      unset ($config->lti_description);
2811      unset ($config->lti_ltiversion);
2812      unset ($config->lti_clientid);
2813      unset ($config->lti_icon);
2814      unset ($config->lti_secureicon);
2815  }
2816  
2817  function lti_update_type($type, $config) {
2818      global $DB, $CFG;
2819  
2820      lti_prepare_type_for_save($type, $config);
2821  
2822      if (lti_request_is_using_ssl() && !empty($type->secureicon)) {
2823          $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);
2824      } else {
2825          $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon));
2826      }
2827      unset($config->oldicon);
2828  
2829      if ($DB->update_record('lti_types', $type)) {
2830          foreach ($config as $key => $value) {
2831              if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {
2832                  $record = new \StdClass();
2833                  $record->typeid = $type->id;
2834                  $record->name = substr($key, 4);
2835                  $record->value = $value;
2836                  lti_update_config($record);
2837              }
2838              if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
2839                  $record = new \StdClass();
2840                  $record->typeid = $type->id;
2841                  $record->name = $key;
2842                  $record->value = $value;
2843                  lti_update_config($record);
2844              }
2845          }
2846          if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) {
2847              // We need to remove the tool proxy for this tool to function under 1.3.
2848              $toolproxyid = $type->toolproxyid;
2849              $DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid));
2850              $DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid));
2851              $type->toolproxyid = null;
2852              $DB->update_record('lti_types', $type);
2853          }
2854          require_once($CFG->libdir.'/modinfolib.php');
2855          if ($clearcache) {
2856              $sql = "SELECT cm.id, cm.course
2857                        FROM {course_modules} cm
2858                        JOIN {modules} m ON cm.module = m.id
2859                        JOIN {lti} l ON l.course = cm.course
2860                       WHERE m.name = :name AND l.typeid = :typeid";
2861  
2862              $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]);
2863  
2864              $courseids = [];
2865              foreach ($rs as $record) {
2866                  $courseids[] = $record->course;
2867                  \course_modinfo::purge_course_module_cache($record->course, $record->id);
2868              }
2869              $rs->close();
2870              $courseids = array_unique($courseids);
2871              foreach ($courseids as $courseid) {
2872                  rebuild_course_cache($courseid, false, true);
2873              }
2874          }
2875      }
2876  }
2877  
2878  function lti_add_type($type, $config) {
2879      global $USER, $SITE, $DB;
2880  
2881      lti_prepare_type_for_save($type, $config);
2882  
2883      if (!isset($type->state)) {
2884          $type->state = LTI_TOOL_STATE_PENDING;
2885      }
2886  
2887      if (!isset($type->ltiversion)) {
2888          $type->ltiversion = LTI_VERSION_1;
2889      }
2890  
2891      if (!isset($type->timecreated)) {
2892          $type->timecreated = time();
2893      }
2894  
2895      if (!isset($type->createdby)) {
2896          $type->createdby = $USER->id;
2897      }
2898  
2899      if (!isset($type->course)) {
2900          $type->course = $SITE->id;
2901      }
2902  
2903      // Create a salt value to be used for signing passed data to extension services
2904      // The outcome service uses the service salt on the instance. This can be used
2905      // for communication with services not related to a specific LTI instance.
2906      $config->lti_servicesalt = uniqid('', true);
2907  
2908      $id = $DB->insert_record('lti_types', $type);
2909  
2910      if ($id) {
2911          foreach ($config as $key => $value) {
2912              if (!is_null($value)) {
2913                  if (substr($key, 0, 4) === 'lti_') {
2914                      $fieldname = substr($key, 4);
2915                  } else if (substr($key, 0, 11) !== 'ltiservice_') {
2916                      continue;
2917                  } else {
2918                      $fieldname = $key;
2919                  }
2920  
2921                  $record = new \StdClass();
2922                  $record->typeid = $id;
2923                  $record->name = $fieldname;
2924                  $record->value = $value;
2925  
2926                  lti_add_config($record);
2927              }
2928          }
2929      }
2930  
2931      return $id;
2932  }
2933  
2934  /**
2935   * Given an array of tool proxies, filter them based on their state
2936   *
2937   * @param array $toolproxies An array of lti_tool_proxies records
2938   * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants
2939   *
2940   * @return array
2941   */
2942  function lti_filter_tool_proxy_types(array $toolproxies, $state) {
2943      $return = array();
2944      foreach ($toolproxies as $key => $toolproxy) {
2945          if ($toolproxy->state == $state) {
2946              $return[$key] = $toolproxy;
2947          }
2948      }
2949      return $return;
2950  }
2951  
2952  /**
2953   * Get the tool proxy instance given its GUID
2954   *
2955   * @param string  $toolproxyguid   Tool proxy GUID value
2956   *
2957   * @return object
2958   */
2959  function lti_get_tool_proxy_from_guid($toolproxyguid) {
2960      global $DB;
2961  
2962      $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid));
2963  
2964      return $toolproxy;
2965  }
2966  
2967  /**
2968   * Get the tool proxy instance given its registration URL
2969   *
2970   * @param string $regurl Tool proxy registration URL
2971   *
2972   * @return array The record of the tool proxy with this url
2973   */
2974  function lti_get_tool_proxies_from_registration_url($regurl) {
2975      global $DB;
2976  
2977      return $DB->get_records_sql(
2978          'SELECT * FROM {lti_tool_proxies}
2979          WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl',
2980          array('regurl' => $regurl)
2981      );
2982  }
2983  
2984  /**
2985   * Generates some of the tool proxy configuration based on the admin configuration details
2986   *
2987   * @param int $id
2988   *
2989   * @return mixed Tool Proxy details
2990   */
2991  function lti_get_tool_proxy($id) {
2992      global $DB;
2993  
2994      $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id));
2995      return $toolproxy;
2996  }
2997  
2998  /**
2999   * Returns lti tool proxies.
3000   *
3001   * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them
3002   * @return array of basicLTI types
3003   */
3004  function lti_get_tool_proxies($orphanedonly) {
3005      global $DB;
3006  
3007      if ($orphanedonly) {
3008          $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));
3009          $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3010          foreach ($proxies as $key => $value) {
3011              if (in_array($value->id, $usedproxyids)) {
3012                  unset($proxies[$key]);
3013              }
3014          }
3015          return $proxies;
3016      } else {
3017          return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3018      }
3019  }
3020  
3021  /**
3022   * Generates some of the tool proxy configuration based on the admin configuration details
3023   *
3024   * @param int $id
3025   *
3026   * @return mixed  Tool Proxy details
3027   */
3028  function lti_get_tool_proxy_config($id) {
3029      $toolproxy = lti_get_tool_proxy($id);
3030  
3031      $tp = new \stdClass();
3032      $tp->lti_registrationname = $toolproxy->name;
3033      $tp->toolproxyid = $toolproxy->id;
3034      $tp->state = $toolproxy->state;
3035      $tp->lti_registrationurl = $toolproxy->regurl;
3036      $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered);
3037      $tp->lti_services = explode("\n", $toolproxy->serviceoffered);
3038  
3039      return $tp;
3040  }
3041  
3042  /**
3043   * Update the database with a tool proxy instance
3044   *
3045   * @param object   $config    Tool proxy definition
3046   *
3047   * @return int  Record id number
3048   */
3049  function lti_add_tool_proxy($config) {
3050      global $USER, $DB;
3051  
3052      $toolproxy = new \stdClass();
3053      if (isset($config->lti_registrationname)) {
3054          $toolproxy->name = trim($config->lti_registrationname);
3055      }
3056      if (isset($config->lti_registrationurl)) {
3057          $toolproxy->regurl = trim($config->lti_registrationurl);
3058      }
3059      if (isset($config->lti_capabilities)) {
3060          $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities);
3061      } else {
3062          $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities()));
3063      }
3064      if (isset($config->lti_services)) {
3065          $toolproxy->serviceoffered = implode("\n", $config->lti_services);
3066      } else {
3067          $func = function($s) {
3068              return $s->get_id();
3069          };
3070          $servicenames = array_map($func, lti_get_services());
3071          $toolproxy->serviceoffered = implode("\n", $servicenames);
3072      }
3073      if (isset($config->toolproxyid) && !empty($config->toolproxyid)) {
3074          $toolproxy->id = $config->toolproxyid;
3075          if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) {
3076              $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3077              $toolproxy->guid = random_string();
3078              $toolproxy->secret = random_string();
3079          }
3080          $id = lti_update_tool_proxy($toolproxy);
3081      } else {
3082          $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3083          $toolproxy->timemodified = time();
3084          $toolproxy->timecreated = $toolproxy->timemodified;
3085          if (!isset($toolproxy->createdby)) {
3086              $toolproxy->createdby = $USER->id;
3087          }
3088          $toolproxy->guid = random_string();
3089          $toolproxy->secret = random_string();
3090          $id = $DB->insert_record('lti_tool_proxies', $toolproxy);
3091      }
3092  
3093      return $id;
3094  }
3095  
3096  /**
3097   * Updates a tool proxy in the database
3098   *
3099   * @param object  $toolproxy   Tool proxy
3100   *
3101   * @return int    Record id number
3102   */
3103  function lti_update_tool_proxy($toolproxy) {
3104      global $DB;
3105  
3106      $toolproxy->timemodified = time();
3107      $id = $DB->update_record('lti_tool_proxies', $toolproxy);
3108  
3109      return $id;
3110  }
3111  
3112  /**
3113   * Delete a Tool Proxy
3114   *
3115   * @param int $id   Tool Proxy id
3116   */
3117  function lti_delete_tool_proxy($id) {
3118      global $DB;
3119      $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id));
3120      $tools = $DB->get_records('lti_types', array('toolproxyid' => $id));
3121      foreach ($tools as $tool) {
3122          lti_delete_type($tool->id);
3123      }
3124      $DB->delete_records('lti_tool_proxies', array('id' => $id));
3125  }
3126  
3127  /**
3128   * Get both LTI tool proxies and tool types.
3129   *
3130   * If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool
3131   * types.
3132   * For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types
3133   * will be returned.
3134   *
3135   * @param int $limit Maximum number of tools returned.
3136   * @param int $offset Do not return tools before offset index.
3137   * @param bool $orphanedonly If true, only return orphaned proxies.
3138   * @param int $toolproxyid If not 0, only return tool types that have this tool proxy id.
3139   * @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types.
3140   */
3141  function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array {
3142      global $DB;
3143  
3144      if ($orphanedonly) {
3145          $orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false);
3146          $countsql = helper::get_tool_proxy_sql($orphanedonly, true);
3147          $proxies  = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit);
3148          $totalproxiescount = $DB->count_records_sql($countsql);
3149      } else {
3150          $proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC',
3151              '*', $offset, $limit);
3152          $totalproxiescount = $DB->count_records('lti_tool_proxies');
3153      }
3154  
3155      // Find new offset and limit for tool types after getting proxies and set up query.
3156      $typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative.
3157      $typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative.
3158      $typesparams = [];
3159      if (!empty($toolproxyid)) {
3160          $typesparams['toolproxyid'] = $toolproxyid;
3161      }
3162  
3163      $types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC',
3164              '*', $typesoffset, $typeslimit);
3165  
3166      return [$proxies, array_map('serialise_tool_type', $types)];
3167  }
3168  
3169  /**
3170   * Get the total number of LTI tool types and tool proxies.
3171   *
3172   * @param bool $orphanedonly If true, only count orphaned proxies.
3173   * @param int $toolproxyid If not 0, only count tool types that have this tool proxy id.
3174   * @return int Count of tools.
3175   */
3176  function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int {
3177      global $DB;
3178  
3179      $typessql = "SELECT count(*)
3180                     FROM {lti_types}";
3181      $typesparams = [];
3182      if (!empty($toolproxyid)) {
3183          $typessql .= " WHERE toolproxyid = :toolproxyid";
3184          $typesparams['toolproxyid'] = $toolproxyid;
3185      }
3186  
3187      $proxiessql = helper::get_tool_proxy_sql($orphanedonly, true);
3188  
3189      $countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause();
3190  
3191      return $DB->count_records_sql($countsql, $typesparams);
3192  }
3193  
3194  /**
3195   * Add a tool configuration in the database
3196   *
3197   * @param object $config   Tool configuration
3198   *
3199   * @return int Record id number
3200   */
3201  function lti_add_config($config) {
3202      global $DB;
3203  
3204      return $DB->insert_record('lti_types_config', $config);
3205  }
3206  
3207  /**
3208   * Updates a tool configuration in the database
3209   *
3210   * @param object  $config   Tool configuration
3211   *
3212   * @return mixed Record id number
3213   */
3214  function lti_update_config($config) {
3215      global $DB;
3216  
3217      $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));
3218  
3219      if ($old) {
3220          $config->id = $old->id;
3221          $return = $DB->update_record('lti_types_config', $config);
3222      } else {
3223          $return = $DB->insert_record('lti_types_config', $config);
3224      }
3225      return $return;
3226  }
3227  
3228  /**
3229   * Gets the tool settings
3230   *
3231   * @param int  $toolproxyid   Id of tool proxy record (or tool ID if negative)
3232   * @param int  $courseid      Id of course (null if system settings)
3233   * @param int  $instanceid    Id of course module (null if system or context settings)
3234   *
3235   * @return array  Array settings
3236   */
3237  function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) {
3238      global $DB;
3239  
3240      $settings = array();
3241      if ($toolproxyid > 0) {
3242          $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
3243              'course' => $courseid, 'coursemoduleid' => $instanceid));
3244      } else {
3245          $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid,
3246              'course' => $courseid, 'coursemoduleid' => $instanceid));
3247      }
3248      if ($settingsstr !== false) {
3249          $settings = json_decode($settingsstr, true);
3250      }
3251      return $settings;
3252  }
3253  
3254  /**
3255   * Sets the tool settings (
3256   *
3257   * @param array  $settings      Array of settings
3258   * @param int    $toolproxyid   Id of tool proxy record (or tool ID if negative)
3259   * @param int    $courseid      Id of course (null if system settings)
3260   * @param int    $instanceid    Id of course module (null if system or context settings)
3261   */
3262  function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) {
3263      global $DB;
3264  
3265      $json = json_encode($settings);
3266      if ($toolproxyid >= 0) {
3267          $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
3268              'course' => $courseid, 'coursemoduleid' => $instanceid));
3269      } else {
3270          $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid,
3271              'course' => $courseid, 'coursemoduleid' => $instanceid));
3272      }
3273      if ($record !== false) {
3274          $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
3275      } else {
3276          $record = new \stdClass();
3277          if ($toolproxyid > 0) {
3278              $record->toolproxyid = $toolproxyid;
3279          } else {
3280              $record->typeid = -$toolproxyid;
3281          }
3282          $record->course = $courseid;
3283          $record->coursemoduleid = $instanceid;
3284          $record->settings = $json;
3285          $record->timecreated = time();
3286          $record->timemodified = $record->timecreated;
3287          $DB->insert_record('lti_tool_settings', $record);
3288      }
3289  }
3290  
3291  /**
3292   * Signs the petition to launch the external tool using OAuth
3293   *
3294   * @param array  $oldparms     Parameters to be passed for signing
3295   * @param string $endpoint     url of the external tool
3296   * @param string $method       Method for sending the parameters (e.g. POST)
3297   * @param string $oauthconsumerkey
3298   * @param string $oauthconsumersecret
3299   * @return array|null
3300   */
3301  function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
3302  
3303      $parms = $oldparms;
3304  
3305      $testtoken = '';
3306  
3307      // TODO: Switch to core oauthlib once implemented - MDL-30149.
3308      $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1();
3309      $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null);
3310      $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms);
3311      $accreq->sign_request($hmacmethod, $testconsumer, $testtoken);
3312  
3313      $newparms = $accreq->get_parameters();
3314  
3315      return $newparms;
3316  }
3317  
3318  /**
3319   * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT
3320   *
3321   * @param array  $parms        Parameters to be passed for signing
3322   * @param string $endpoint     url of the external tool
3323   * @param string $oauthconsumerkey
3324   * @param string $typeid       ID of LTI tool type
3325   * @param string $nonce        Nonce value to use
3326   * @return array|null
3327   */
3328  function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {
3329      global $CFG;
3330  
3331      if (empty($typeid)) {
3332          $typeid = 0;
3333      }
3334      $messagetypemapping = lti_get_jwt_message_type_mapping();
3335      if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) {
3336          $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']];
3337      }
3338      if (isset($parms['roles'])) {
3339          $roles = explode(',', $parms['roles']);
3340          $newroles = array();
3341          foreach ($roles as $role) {
3342              if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {
3343                  $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);
3344              } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {
3345                  $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);
3346              } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {
3347                  $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);
3348              } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {
3349                  $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";
3350              }
3351              $newroles[] = $role;
3352          }
3353          $parms['roles'] = implode(',', $newroles);
3354      }
3355  
3356      $now = time();
3357      if (empty($nonce)) {
3358          $nonce = bin2hex(openssl_random_pseudo_bytes(10));
3359      }
3360      $claimmapping = lti_get_jwt_claim_mapping();
3361      $payload = array(
3362          'nonce' => $nonce,
3363          'iat' => $now,
3364          'exp' => $now + 60,
3365      );
3366      $payload['iss'] = $CFG->wwwroot;
3367      $payload['aud'] = $oauthconsumerkey;
3368      $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid);
3369      $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;
3370  
3371      foreach ($parms as $key => $value) {
3372          $claim = LTI_JWT_CLAIM_PREFIX;
3373          if (array_key_exists($key, $claimmapping)) {
3374              $mapping = $claimmapping[$key];
3375              $type = $mapping["type"] ?? "string";
3376              if ($mapping['isarray']) {
3377                  $value = explode(',', $value);
3378                  sort($value);
3379              } else if ($type == 'boolean') {
3380                  $value = isset($value) && ($value == 'true');
3381              }
3382              if (!empty($mapping['suffix'])) {
3383                  $claim .= "-{$mapping['suffix']}";
3384              }
3385              $claim .= '/claim/';
3386              if (is_null($mapping['group'])) {
3387                  $payload[$mapping['claim']] = $value;
3388              } else if (empty($mapping['group'])) {
3389                  $payload["{$claim}{$mapping['claim']}"] = $value;
3390              } else {
3391                  $claim .= $mapping['group'];
3392                  $payload[$claim][$mapping['claim']] = $value;
3393              }
3394          } else if (strpos($key, 'custom_') === 0) {
3395              $payload["{$claim}/claim/custom"][substr($key, 7)] = $value;
3396          } else if (strpos($key, 'ext_') === 0) {
3397              $payload["{$claim}/claim/ext"][substr($key, 4)] = $value;
3398          }
3399      }
3400  
3401      $privatekey = jwks_helper::get_private_key();
3402      $jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']);
3403  
3404      $newparms = array();
3405      $newparms['id_token'] = $jwt;
3406  
3407      return $newparms;
3408  }
3409  
3410  /**
3411   * Verfies the JWT and converts its claims to their equivalent message parameter.
3412   *
3413   * @param int    $typeid
3414   * @param string $jwtparam   JWT parameter
3415   *
3416   * @return array  message parameters
3417   * @throws moodle_exception
3418   */
3419  function lti_convert_from_jwt($typeid, $jwtparam) {
3420  
3421      $params = array();
3422      $parts = explode('.', $jwtparam);
3423      $ok = (count($parts) === 3);
3424      if ($ok) {
3425          $payload = JWT::urlsafeB64Decode($parts[1]);
3426          $claims = json_decode($payload, true);
3427          $ok = !is_null($claims) && !empty($claims['iss']);
3428      }
3429      if ($ok) {
3430          lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);
3431          $params['oauth_consumer_key'] = $claims['iss'];
3432          foreach (lti_get_jwt_claim_mapping() as $key => $mapping) {
3433              $claim = LTI_JWT_CLAIM_PREFIX;
3434              if (!empty($mapping['suffix'])) {
3435                  $claim .= "-{$mapping['suffix']}";
3436              }
3437              $claim .= '/claim/';
3438              if (is_null($mapping['group'])) {
3439                  $claim = $mapping['claim'];
3440              } else if (empty($mapping['group'])) {
3441                  $claim .= $mapping['claim'];
3442              } else {
3443                  $claim .= $mapping['group'];
3444              }
3445              if (isset($claims[$claim])) {
3446                  $value = null;
3447                  if (empty($mapping['group'])) {
3448                      $value = $claims[$claim];
3449                  } else {
3450                      $group = $claims[$claim];
3451                      if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
3452                          $value = $group[$mapping['claim']];
3453                      }
3454                  }
3455                  if (!empty($value) && $mapping['isarray']) {
3456                      if (is_array($value)) {
3457                          if (is_array($value[0])) {
3458                              $value = json_encode($value);
3459                          } else {
3460                              $value = implode(',', $value);
3461                          }
3462                      }
3463                  }
3464                  if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {
3465                      $params[$key] = $value;
3466                  }
3467              }
3468              $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';
3469              if (isset($claims[$claim])) {
3470                  $custom = $claims[$claim];
3471                  if (is_array($custom)) {
3472                      foreach ($custom as $key => $value) {
3473                          $params["custom_{$key}"] = $value;
3474                      }
3475                  }
3476              }
3477              $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';
3478              if (isset($claims[$claim])) {
3479                  $ext = $claims[$claim];
3480                  if (is_array($ext)) {
3481                      foreach ($ext as $key => $value) {
3482                          $params["ext_{$key}"] = $value;
3483                      }
3484                  }
3485              }
3486          }
3487      }
3488      if (isset($params['content_items'])) {
3489          $params['content_items'] = lti_convert_content_items($params['content_items']);
3490      }
3491      $messagetypemapping = lti_get_jwt_message_type_mapping();
3492      if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) {
3493          $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']];
3494      }
3495      return $params;
3496  }
3497  
3498  /**
3499   * Posts the launch petition HTML
3500   *
3501   * @param array $newparms   Signed parameters
3502   * @param string $endpoint  URL of the external tool
3503   * @param bool $debug       Debug (true/false)
3504   * @return string
3505   */
3506  function lti_post_launch_html($newparms, $endpoint, $debug=false) {
3507      $r = "<form action=\"" . $endpoint .
3508          "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n";
3509  
3510      // Contruct html for the launch parameters.
3511      foreach ($newparms as $key => $value) {
3512          $key = htmlspecialchars($key, ENT_COMPAT);
3513          $value = htmlspecialchars($value, ENT_COMPAT);
3514          if ( $key == "ext_submit" ) {
3515              $r .= "<input type=\"submit\"";
3516          } else {
3517              $r .= "<input type=\"hidden\" name=\"{$key}\"";
3518          }
3519          $r .= " value=\"";
3520          $r .= $value;
3521          $r .= "\"/>\n";
3522      }
3523  
3524      if ( $debug ) {
3525          $r .= "<script language=\"javascript\"> \n";
3526          $r .= "  //<![CDATA[ \n";
3527          $r .= "function basicltiDebugToggle() {\n";
3528          $r .= "    var ele = document.getElementById(\"basicltiDebug\");\n";
3529          $r .= "    if (ele.style.display == \"block\") {\n";
3530          $r .= "        ele.style.display = \"none\";\n";
3531          $r .= "    }\n";
3532          $r .= "    else {\n";
3533          $r .= "        ele.style.display = \"block\";\n";
3534          $r .= "    }\n";
3535          $r .= "} \n";
3536          $r .= "  //]]> \n";
3537          $r .= "</script>\n";
3538          $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";
3539          $r .= get_string("toggle_debug_data", "lti")."</a>\n";
3540          $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
3541          $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n";
3542          $r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
3543          $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n";
3544          foreach ($newparms as $key => $value) {
3545              $key = htmlspecialchars($key, ENT_COMPAT);
3546              $value = htmlspecialchars($value, ENT_COMPAT);
3547              $r .= "$key = $value<br/>\n";
3548          }
3549          $r .= "&nbsp;<br/>\n";
3550          $r .= "</div>\n";
3551      }
3552      $r .= "</form>\n";
3553  
3554      // Auto-submit the form if endpoint is set.
3555      if ($endpoint !== '' && !$debug) {
3556          $r .= " <script type=\"text/javascript\"> \n" .
3557              "  //<![CDATA[ \n" .
3558              "    document.ltiLaunchForm.submit(); \n" .
3559              "  //]]> \n" .
3560              " </script> \n";
3561      }
3562      return $r;
3563  }
3564  
3565  /**
3566   * Generate the form for initiating a login request for an LTI 1.3 message
3567   *
3568   * @param int            $courseid  Course ID
3569   * @param int            $cmid        LTI instance ID
3570   * @param stdClass|null  $instance  LTI instance
3571   * @param stdClass       $config    Tool type configuration
3572   * @param string         $messagetype   LTI message type
3573   * @param string         $title     Title of content item
3574   * @param string         $text      Description of content item
3575   * @param int            $foruserid Id of the user targeted by the launch
3576   * @return string
3577   */
3578  function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request',
3579          $title = '', $text = '', $foruserid = 0) {
3580      global $SESSION;
3581  
3582      $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text);
3583  
3584      $r = "<form action=\"" . $config->lti_initiatelogin .
3585          "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " .
3586          "encType=\"application/x-www-form-urlencoded\">\n";
3587  
3588      foreach ($params as $key => $value) {
3589          $key = htmlspecialchars($key, ENT_COMPAT);
3590          $value = htmlspecialchars($value, ENT_COMPAT);
3591          $r .= "  <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
3592      }
3593      $r .= "</form>\n";
3594  
3595      $r .= "<script type=\"text/javascript\">\n" .
3596          "//<![CDATA[\n" .
3597          "document.ltiInitiateLoginForm.submit();\n" .
3598          "//]]>\n" .
3599          "</script>\n";
3600  
3601      return $r;
3602  }
3603  
3604  /**
3605   * Prepares an LTI 1.3 login request
3606   *
3607   * @param int            $courseid  Course ID
3608   * @param int            $cmid        Course Module instance ID
3609   * @param stdClass|null  $instance  LTI instance
3610   * @param stdClass       $config    Tool type configuration
3611   * @param string         $messagetype   LTI message type
3612   * @param int            $foruserid Id of the user targeted by the launch
3613   * @param string         $title     Title of content item
3614   * @param string         $text      Description of content item
3615   * @return array Login request parameters
3616   */
3617  function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') {
3618      global $USER, $CFG, $SESSION;
3619      $ltihint = [];
3620      if (!empty($instance)) {
3621          $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;
3622          $launchid = 'ltilaunch'.$instance->id.'_'.rand();
3623          $ltihint['cmid'] = $cmid;
3624          $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,";
3625      } else {
3626          $endpoint = $config->lti_toolurl;
3627          if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) {
3628              $endpoint = $config->lti_toolurl_ContentItemSelectionRequest;
3629          }
3630          $launchid = "ltilaunch_$messagetype".rand();
3631          $SESSION->$launchid =
3632              "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text);
3633      }
3634      $endpoint = trim($endpoint);
3635      $services = lti_get_services();
3636      foreach ($services as $service) {
3637          [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance);
3638      }
3639  
3640      $ltihint['launchid'] = $launchid;
3641      // If SSL is forced make sure https is on the normal launch URL.
3642      if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {
3643          $endpoint = lti_ensure_url_is_https($endpoint);
3644      } else if (!strstr($endpoint, '://')) {
3645          $endpoint = 'http://' . $endpoint;
3646      }
3647  
3648      $params = array();
3649      $params['iss'] = $CFG->wwwroot;
3650      $params['target_link_uri'] = $endpoint;
3651      $params['login_hint'] = $USER->id;
3652      $params['lti_message_hint'] = json_encode($ltihint);
3653      $params['client_id'] = $config->lti_clientid;
3654      $params['lti_deployment_id'] = $config->typeid;
3655      return $params;
3656  }
3657  
3658  function lti_get_type($typeid) {
3659      global $DB;
3660  
3661      return $DB->get_record('lti_types', array('id' => $typeid));
3662  }
3663  
3664  function lti_get_launch_container($lti, $toolconfig) {
3665      if (empty($lti->launchcontainer)) {
3666          $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
3667      }
3668  
3669      if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3670          if (isset($toolconfig['launchcontainer'])) {
3671              $launchcontainer = $toolconfig['launchcontainer'];
3672          }
3673      } else {
3674          $launchcontainer = $lti->launchcontainer;
3675      }
3676  
3677      if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3678          $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
3679      }
3680  
3681      $devicetype = core_useragent::get_device_type();
3682  
3683      // Scrolling within the object element doesn't work on iOS or Android
3684      // Opening the popup window also had some issues in testing
3685      // For mobile devices, always take up the entire screen to ensure the best experience.
3686      if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) {
3687          $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW;
3688      }
3689  
3690      return $launchcontainer;
3691  }
3692  
3693  function lti_request_is_using_ssl() {
3694      global $CFG;
3695      return (stripos($CFG->wwwroot, 'https://') === 0);
3696  }
3697  
3698  function lti_ensure_url_is_https($url) {
3699      if (!strstr($url, '://')) {
3700          $url = 'https://' . $url;
3701      } else {
3702          // If the URL starts with http, replace with https.
3703          if (stripos($url, 'http://') === 0) {
3704              $url = 'https://' . substr($url, 7);
3705          }
3706      }
3707  
3708      return $url;
3709  }
3710  
3711  /**
3712   * Determines if we should try to log the request
3713   *
3714   * @param string $rawbody
3715   * @return bool
3716   */
3717  function lti_should_log_request($rawbody) {
3718      global $CFG;
3719  
3720      if (empty($CFG->mod_lti_log_users)) {
3721          return false;
3722      }
3723  
3724      $logusers = explode(',', $CFG->mod_lti_log_users);
3725      if (empty($logusers)) {
3726          return false;
3727      }
3728  
3729      try {
3730          $xml = new \SimpleXMLElement($rawbody);
3731          $ns  = $xml->getNamespaces();
3732          $ns  = array_shift($ns);
3733          $xml->registerXPathNamespace('lti', $ns);
3734          $requestuserid = '';
3735          if ($node = $xml->xpath('//lti:userId')) {
3736              $node = $node[0];
3737              $requestuserid = clean_param((string) $node, PARAM_INT);
3738          } else if ($node = $xml->xpath('//lti:sourcedId')) {
3739              $node = $node[0];
3740              $resultjson = json_decode((string) $node);
3741              $requestuserid = clean_param($resultjson->data->userid, PARAM_INT);
3742          }
3743      } catch (Exception $e) {
3744          return false;
3745      }
3746  
3747      if (empty($requestuserid) or !in_array($requestuserid, $logusers)) {
3748          return false;
3749      }
3750  
3751      return true;
3752  }
3753  
3754  /**
3755   * Logs the request to a file in temp dir.
3756   *
3757   * @param string $rawbody
3758   */
3759  function lti_log_request($rawbody) {
3760      if ($tempdir = make_temp_directory('mod_lti', false)) {
3761          if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
3762              $content  = "Request Headers:\n";
3763              foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
3764                  $content .= "$header: $value\n";
3765              }
3766              $content .= "Request Body:\n";
3767              $content .= $rawbody;
3768  
3769              file_put_contents($tempfile, $content);
3770              chmod($tempfile, 0644);
3771          }
3772      }
3773  }
3774  
3775  /**
3776   * Log an LTI response.
3777   *
3778   * @param string $responsexml The response XML
3779   * @param Exception $e If there was an exception, pass that too
3780   */
3781  function lti_log_response($responsexml, $e = null) {
3782      if ($tempdir = make_temp_directory('mod_lti', false)) {
3783          if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
3784              $content = '';
3785              if ($e instanceof Exception) {
3786                  $info = get_exception_info($e);
3787  
3788                  $content .= "Exception:\n";
3789                  $content .= "Message: $info->message\n";
3790                  $content .= "Debug info: $info->debuginfo\n";
3791                  $content .= "Backtrace:\n";
3792                  $content .= format_backtrace($info->backtrace, true);
3793                  $content .= "\n";
3794              }
3795              $content .= "Response XML:\n";
3796              $content .= $responsexml;
3797  
3798              file_put_contents($tempfile, $content);
3799              chmod($tempfile, 0644);
3800          }
3801      }
3802  }
3803  
3804  /**
3805   * Fetches LTI type configuration for an LTI instance
3806   *
3807   * @param stdClass $instance
3808   * @return array Can be empty if no type is found
3809   */
3810  function lti_get_type_config_by_instance($instance) {
3811      $typeid = null;
3812      if (empty($instance->typeid)) {
3813          $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
3814          if ($tool) {
3815              $typeid = $tool->id;
3816          }
3817      } else {
3818          $typeid = $instance->typeid;
3819      }
3820      if (!empty($typeid)) {
3821          return lti_get_type_config($typeid);
3822      }
3823      return array();
3824  }
3825  
3826  /**
3827   * Enforce type config settings onto the LTI instance
3828   *
3829   * @param stdClass $instance
3830   * @param array $typeconfig
3831   */
3832  function lti_force_type_config_settings($instance, array $typeconfig) {
3833      $forced = array(
3834          'instructorchoicesendname'      => 'sendname',
3835          'instructorchoicesendemailaddr' => 'sendemailaddr',
3836          'instructorchoiceacceptgrades'  => 'acceptgrades',
3837      );
3838  
3839      foreach ($forced as $instanceparam => $typeconfigparam) {
3840          if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) {
3841              $instance->$instanceparam = $typeconfig[$typeconfigparam];
3842          }
3843      }
3844  }
3845  
3846  /**
3847   * Initializes an array with the capabilities supported by the LTI module
3848   *
3849   * @return array List of capability names (without a dollar sign prefix)
3850   */
3851  function lti_get_capabilities() {
3852  
3853      $capabilities = array(
3854         'basic-lti-launch-request' => '',
3855         'ContentItemSelectionRequest' => '',
3856         'ToolProxyRegistrationRequest' => '',
3857         'Context.id' => 'context_id',
3858         'Context.title' => 'context_title',
3859         'Context.label' => 'context_label',
3860         'Context.id.history' => null,
3861         'Context.sourcedId' => 'lis_course_section_sourcedid',
3862         'Context.longDescription' => '$COURSE->summary',
3863         'Context.timeFrame.begin' => '$COURSE->startdate',
3864         'CourseSection.title' => 'context_title',
3865         'CourseSection.label' => 'context_label',
3866         'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
3867         'CourseSection.longDescription' => '$COURSE->summary',
3868         'CourseSection.timeFrame.begin' => null,
3869         'CourseSection.timeFrame.end' => null,
3870         'ResourceLink.id' => 'resource_link_id',
3871         'ResourceLink.title' => 'resource_link_title',
3872         'ResourceLink.description' => 'resource_link_description',
3873         'User.id' => 'user_id',
3874         'User.username' => '$USER->username',
3875         'Person.name.full' => 'lis_person_name_full',
3876         'Person.name.given' => 'lis_person_name_given',
3877         'Person.name.family' => 'lis_person_name_family',
3878         'Person.email.primary' => 'lis_person_contact_email_primary',
3879         'Person.sourcedId' => 'lis_person_sourcedid',
3880         'Person.name.middle' => '$USER->middlename',
3881         'Person.address.street1' => '$USER->address',
3882         'Person.address.locality' => '$USER->city',
3883         'Person.address.country' => '$USER->country',
3884         'Person.address.timezone' => '$USER->timezone',
3885         'Person.phone.primary' => '$USER->phone1',
3886         'Person.phone.mobile' => '$USER->phone2',
3887         'Person.webaddress' => '$USER->url',
3888         'Membership.role' => 'roles',
3889         'Result.sourcedId' => 'lis_result_sourcedid',
3890         'Result.autocreate' => 'lis_outcome_service_url',
3891         'BasicOutcome.sourcedId' => 'lis_result_sourcedid',
3892         'BasicOutcome.url' => 'lis_outcome_service_url',
3893         'Moodle.Person.userGroupIds' => null);
3894  
3895      return $capabilities;
3896  
3897  }
3898  
3899  /**
3900   * Initializes an array with the services supported by the LTI module
3901   *
3902   * @return array List of services
3903   */
3904  function lti_get_services() {
3905  
3906      $services = array();
3907      $definedservices = core_component::get_plugin_list('ltiservice');
3908      foreach ($definedservices as $name => $location) {
3909          $classname = "\\ltiservice_{$name}\\local\\service\\{$name}";
3910          $services[] = new $classname();
3911      }
3912  
3913      return $services;
3914  
3915  }
3916  
3917  /**
3918   * Initializes an instance of the named service
3919   *
3920   * @param string $servicename Name of service
3921   *
3922   * @return bool|\mod_lti\local\ltiservice\service_base Service
3923   */
3924  function lti_get_service_by_name($servicename) {
3925  
3926      $service = false;
3927      $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}";
3928      if (class_exists($classname)) {
3929          $service = new $classname();
3930      }
3931  
3932      return $service;
3933  
3934  }
3935  
3936  /**
3937   * Finds a service by id
3938   *
3939   * @param \mod_lti\local\ltiservice\service_base[] $services Array of services
3940   * @param string $resourceid  ID of resource
3941   *
3942   * @return mod_lti\local\ltiservice\service_base Service
3943   */
3944  function lti_get_service_by_resource_id($services, $resourceid) {
3945  
3946      $service = false;
3947      foreach ($services as $aservice) {
3948          foreach ($aservice->get_resources() as $resource) {
3949              if ($resource->get_id() === $resourceid) {
3950                  $service = $aservice;
3951                  break 2;
3952              }
3953          }
3954      }
3955  
3956      return $service;
3957  
3958  }
3959  
3960  /**
3961   * Initializes an array with the scopes for services supported by the LTI module
3962   * and authorized for this particular tool instance.
3963   *
3964   * @param object $type  LTI tool type
3965   * @param array  $typeconfig  LTI tool type configuration
3966   *
3967   * @return array List of scopes
3968   */
3969  function lti_get_permitted_service_scopes($type, $typeconfig) {
3970  
3971      $services = lti_get_services();
3972      $scopes = array();
3973      foreach ($services as $service) {
3974          $service->set_type($type);
3975          $service->set_typeconfig($typeconfig);
3976          $servicescopes = $service->get_permitted_scopes();
3977          if (!empty($servicescopes)) {
3978              $scopes = array_merge($scopes, $servicescopes);
3979          }
3980      }
3981  
3982      return $scopes;
3983  }
3984  
3985  /**
3986   * Extracts the named contexts from a tool proxy
3987   *
3988   * @param object $json
3989   *
3990   * @return array Contexts
3991   */
3992  function lti_get_contexts($json) {
3993  
3994      $contexts = array();
3995      if (isset($json->{'@context'})) {
3996          foreach ($json->{'@context'} as $context) {
3997              if (is_object($context)) {
3998                  $contexts = array_merge(get_object_vars($context), $contexts);
3999              }
4000          }
4001      }
4002  
4003      return $contexts;
4004  
4005  }
4006  
4007  /**
4008   * Converts an ID to a fully-qualified ID
4009   *
4010   * @param array $contexts
4011   * @param string $id
4012   *
4013   * @return string Fully-qualified ID
4014   */
4015  function lti_get_fqid($contexts, $id) {
4016  
4017      $parts = explode(':', $id, 2);
4018      if (count($parts) > 1) {
4019          if (array_key_exists($parts[0], $contexts)) {
4020              $id = $contexts[$parts[0]] . $parts[1];
4021          }
4022      }
4023  
4024      return $id;
4025  
4026  }
4027  
4028  /**
4029   * Returns the icon for the given tool type
4030   *
4031   * @param stdClass $type The tool type
4032   *
4033   * @return string The url to the tool type's corresponding icon
4034   */
4035  function get_tool_type_icon_url(stdClass $type) {
4036      global $OUTPUT;
4037  
4038      $iconurl = $type->secureicon;
4039  
4040      if (empty($iconurl)) {
4041          $iconurl = $type->icon;
4042      }
4043  
4044      if (empty($iconurl)) {
4045          $iconurl = $OUTPUT->image_url('monologo', 'lti')->out();
4046      }
4047  
4048      return $iconurl;
4049  }
4050  
4051  /**
4052   * Returns the edit url for the given tool type
4053   *
4054   * @param stdClass $type The tool type
4055   *
4056   * @return string The url to edit the tool type
4057   */
4058  function get_tool_type_edit_url(stdClass $type) {
4059      $url = new moodle_url('/mod/lti/typessettings.php',
4060                            array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4061      return $url->out();
4062  }
4063  
4064  /**
4065   * Returns the edit url for the given tool proxy.
4066   *
4067   * @param stdClass $proxy The tool proxy
4068   *
4069   * @return string The url to edit the tool type
4070   */
4071  function get_tool_proxy_edit_url(stdClass $proxy) {
4072      $url = new moodle_url('/mod/lti/registersettings.php',
4073                            array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4074      return $url->out();
4075  }
4076  
4077  /**
4078   * Returns the course url for the given tool type
4079   *
4080   * @param stdClass $type The tool type
4081   *
4082   * @return string The url to the course of the tool type, void if it is a site wide type
4083   */
4084  function get_tool_type_course_url(stdClass $type) {
4085      if ($type->course != 1) {
4086          $url = new moodle_url('/course/view.php', array('id' => $type->course));
4087          return $url->out();
4088      }
4089      return null;
4090  }
4091  
4092  /**
4093   * Returns the icon and edit urls for the tool type and the course url if it is a course type.
4094   *
4095   * @param stdClass $type The tool type
4096   *
4097   * @return array The urls of the tool type
4098   */
4099  function get_tool_type_urls(stdClass $type) {
4100      $courseurl = get_tool_type_course_url($type);
4101  
4102      $urls = array(
4103          'icon' => get_tool_type_icon_url($type),
4104          'edit' => get_tool_type_edit_url($type),
4105      );
4106  
4107      if ($courseurl) {
4108          $urls['course'] = $courseurl;
4109      }
4110  
4111      $url = new moodle_url('/mod/lti/certs.php');
4112      $urls['publickeyset'] = $url->out();
4113      $url = new moodle_url('/mod/lti/token.php');
4114      $urls['accesstoken'] = $url->out();
4115      $url = new moodle_url('/mod/lti/auth.php');
4116      $urls['authrequest'] = $url->out();
4117  
4118      return $urls;
4119  }
4120  
4121  /**
4122   * Returns the icon and edit urls for the tool proxy.
4123   *
4124   * @param stdClass $proxy The tool proxy
4125   *
4126   * @return array The urls of the tool proxy
4127   */
4128  function get_tool_proxy_urls(stdClass $proxy) {
4129      global $OUTPUT;
4130  
4131      $urls = array(
4132          'icon' => $OUTPUT->image_url('monologo', 'lti')->out(),
4133          'edit' => get_tool_proxy_edit_url($proxy),
4134      );
4135  
4136      return $urls;
4137  }
4138  
4139  /**
4140   * Returns information on the current state of the tool type
4141   *
4142   * @param stdClass $type The tool type
4143   *
4144   * @return array An array with a text description of the state, and boolean for whether it is in each state:
4145   * pending, configured, rejected, unknown
4146   */
4147  function get_tool_type_state_info(stdClass $type) {
4148      $isconfigured = false;
4149      $ispending = false;
4150      $isrejected = false;
4151      $isunknown = false;
4152      switch ($type->state) {
4153          case LTI_TOOL_STATE_CONFIGURED:
4154              $state = get_string('active', 'mod_lti');
4155              $isconfigured = true;
4156              break;
4157          case LTI_TOOL_STATE_PENDING:
4158              $state = get_string('pending', 'mod_lti');
4159              $ispending = true;
4160              break;
4161          case LTI_TOOL_STATE_REJECTED:
4162              $state = get_string('rejected', 'mod_lti');
4163              $isrejected = true;
4164              break;
4165          default:
4166              $state = get_string('unknownstate', 'mod_lti');
4167              $isunknown = true;
4168              break;
4169      }
4170  
4171      return array(
4172          'text' => $state,
4173          'pending' => $ispending,
4174          'configured' => $isconfigured,
4175          'rejected' => $isrejected,
4176          'unknown' => $isunknown
4177      );
4178  }
4179  
4180  /**
4181   * Returns information on the configuration of the tool type
4182   *
4183   * @param stdClass $type The tool type
4184   *
4185   * @return array An array with configuration details
4186   */
4187  function get_tool_type_config($type) {
4188      global $CFG;
4189      $platformid = $CFG->wwwroot;
4190      $clientid = $type->clientid;
4191      $deploymentid = $type->id;
4192      $publickeyseturl = new moodle_url('/mod/lti/certs.php');
4193      $publickeyseturl = $publickeyseturl->out();
4194  
4195      $accesstokenurl = new moodle_url('/mod/lti/token.php');
4196      $accesstokenurl = $accesstokenurl->out();
4197  
4198      $authrequesturl = new moodle_url('/mod/lti/auth.php');
4199      $authrequesturl = $authrequesturl->out();
4200  
4201      return array(
4202          'platformid' => $platformid,
4203          'clientid' => $clientid,
4204          'deploymentid' => $deploymentid,
4205          'publickeyseturl' => $publickeyseturl,
4206          'accesstokenurl' => $accesstokenurl,
4207          'authrequesturl' => $authrequesturl
4208      );
4209  }
4210  
4211  /**
4212   * Returns a summary of each LTI capability this tool type requires in plain language
4213   *
4214   * @param stdClass $type The tool type
4215   *
4216   * @return array An array of text descriptions of each of the capabilities this tool type requires
4217   */
4218  function get_tool_type_capability_groups($type) {
4219      $capabilities = lti_get_enabled_capabilities($type);
4220      $groups = array();
4221      $hascourse = false;
4222      $hasactivities = false;
4223      $hasuseraccount = false;
4224      $hasuserpersonal = false;
4225  
4226      foreach ($capabilities as $capability) {
4227          // Bail out early if we've already found all groups.
4228          if (count($groups) >= 4) {
4229              continue;
4230          }
4231  
4232          if (!$hascourse && preg_match('/^CourseSection/', $capability)) {
4233              $hascourse = true;
4234              $groups[] = get_string('courseinformation', 'mod_lti');
4235          } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) {
4236              $hasactivities = true;
4237              $groups[] = get_string('courseactivitiesorresources', 'mod_lti');
4238          } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) {
4239              $hasuseraccount = true;
4240              $groups[] = get_string('useraccountinformation', 'mod_lti');
4241          } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) {
4242              $hasuserpersonal = true;
4243              $groups[] = get_string('userpersonalinformation', 'mod_lti');
4244          }
4245      }
4246  
4247      return $groups;
4248  }
4249  
4250  
4251  /**
4252   * Returns the ids of each instance of this tool type
4253   *
4254   * @param stdClass $type The tool type
4255   *
4256   * @return array An array of ids of the instances of this tool type
4257   */
4258  function get_tool_type_instance_ids($type) {
4259      global $DB;
4260  
4261      return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id)));
4262  }
4263  
4264  /**
4265   * Serialises this tool type
4266   *
4267   * @param stdClass $type The tool type
4268   *
4269   * @return array An array of values representing this type
4270   */
4271  function serialise_tool_type(stdClass $type) {
4272      global $CFG;
4273  
4274      $capabilitygroups = get_tool_type_capability_groups($type);
4275      $instanceids = get_tool_type_instance_ids($type);
4276      // Clean the name. We don't want tags here.
4277      $name = clean_param($type->name, PARAM_NOTAGS);
4278      if (!empty($type->description)) {
4279          // Clean the description. We don't want tags here.
4280          $description = clean_param($type->description, PARAM_NOTAGS);
4281      } else {
4282          $description = get_string('editdescription', 'mod_lti');
4283      }
4284      return array(
4285          'id' => $type->id,
4286          'name' => $name,
4287          'description' => $description,
4288          'urls' => get_tool_type_urls($type),
4289          'state' => get_tool_type_state_info($type),
4290          'platformid' => $CFG->wwwroot,
4291          'clientid' => $type->clientid,
4292          'deploymentid' => $type->id,
4293          'hascapabilitygroups' => !empty($capabilitygroups),
4294          'capabilitygroups' => $capabilitygroups,
4295          // Course ID of 1 means it's not linked to a course.
4296          'courseid' => $type->course == 1 ? 0 : $type->course,
4297          'instanceids' => $instanceids,
4298          'instancecount' => count($instanceids)
4299      );
4300  }
4301  
4302  /**
4303   * Serialises this tool proxy.
4304   *
4305   * @param stdClass $proxy The tool proxy
4306   *
4307   * @deprecated since Moodle 3.10
4308   * @todo This will be finally removed for Moodle 4.2 as part of MDL-69976.
4309   * @return array An array of values representing this type
4310   */
4311  function serialise_tool_proxy(stdClass $proxy) {
4312      $deprecatedtext = __FUNCTION__ . '() is deprecated. Please remove all references to this method.';
4313      debugging($deprecatedtext, DEBUG_DEVELOPER);
4314  
4315      return array(
4316          'id' => $proxy->id,
4317          'name' => $proxy->name,
4318          'description' => get_string('activatetoadddescription', 'mod_lti'),
4319          'urls' => get_tool_proxy_urls($proxy),
4320          'state' => array(
4321              'text' => get_string('pending', 'mod_lti'),
4322              'pending' => true,
4323              'configured' => false,
4324              'rejected' => false,
4325              'unknown' => false
4326          ),
4327          'hascapabilitygroups' => true,
4328          'capabilitygroups' => array(),
4329          'courseid' => 0,
4330          'instanceids' => array(),
4331          'instancecount' => 0
4332      );
4333  }
4334  
4335  /**
4336   * Loads the cartridge information into the tool type, if the launch url is for a cartridge file
4337   *
4338   * @param stdClass $type The tool type object to be filled in
4339   * @since Moodle 3.1
4340   */
4341  function lti_load_type_if_cartridge($type) {
4342      if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) {
4343          lti_load_type_from_cartridge($type->lti_toolurl, $type);
4344      }
4345  }
4346  
4347  /**
4348   * Loads the cartridge information into the new tool, if the launch url is for a cartridge file
4349   *
4350   * @param stdClass $lti The tools config
4351   * @since Moodle 3.1
4352   */
4353  function lti_load_tool_if_cartridge($lti) {
4354      if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) {
4355          lti_load_tool_from_cartridge($lti->toolurl, $lti);
4356      }
4357  }
4358  
4359  /**
4360   * Determines if the given url is for a IMS basic cartridge
4361   *
4362   * @param  string $url The url to be checked
4363   * @return True if the url is for a cartridge
4364   * @since Moodle 3.1
4365   */
4366  function lti_is_cartridge($url) {
4367      // If it is empty, it's not a cartridge.
4368      if (empty($url)) {
4369          return false;
4370      }
4371      // If it has xml at the end of the url, it's a cartridge.
4372      if (preg_match('/\.xml$/', $url)) {
4373          return true;
4374      }
4375      // Even if it doesn't have .xml, load the url to check if it's a cartridge..
4376      try {
4377          $toolinfo = lti_load_cartridge($url,
4378              array(
4379                  "launch_url" => "launchurl"
4380              )
4381          );
4382          if (!empty($toolinfo['launchurl'])) {
4383              return true;
4384          }
4385      } catch (moodle_exception $e) {
4386          return false; // Error loading the xml, so it's not a cartridge.
4387      }
4388      return false;
4389  }
4390  
4391  /**
4392   * Allows you to load settings for an external tool type from an IMS cartridge.
4393   *
4394   * @param  string   $url     The URL to the cartridge
4395   * @param  stdClass $type    The tool type object to be filled in
4396   * @throws moodle_exception if the cartridge could not be loaded correctly
4397   * @since Moodle 3.1
4398   */
4399  function lti_load_type_from_cartridge($url, $type) {
4400      $toolinfo = lti_load_cartridge($url,
4401          array(
4402              "title" => "lti_typename",
4403              "launch_url" => "lti_toolurl",
4404              "description" => "lti_description",
4405              "icon" => "lti_icon",
4406              "secure_icon" => "lti_secureicon"
4407          ),
4408          array(
4409              "icon_url" => "lti_extension_icon",
4410              "secure_icon_url" => "lti_extension_secureicon"
4411          )
4412      );
4413      // If an activity name exists, unset the cartridge name so we don't override it.
4414      if (isset($type->lti_typename)) {
4415          unset($toolinfo['lti_typename']);
4416      }
4417  
4418      // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4419      if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
4420          $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
4421      }
4422      unset($toolinfo['lti_extension_icon']);
4423  
4424      if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
4425          $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
4426      }
4427      unset($toolinfo['lti_extension_secureicon']);
4428  
4429      // Ensure Custom icons aren't overridden by cartridge params.
4430      if (!empty($type->lti_icon)) {
4431          unset($toolinfo['lti_icon']);
4432      }
4433  
4434      if (!empty($type->lti_secureicon)) {
4435          unset($toolinfo['lti_secureicon']);
4436      }
4437  
4438      foreach ($toolinfo as $property => $value) {
4439          $type->$property = $value;
4440      }
4441  }
4442  
4443  /**
4444   * Allows you to load in the configuration for an external tool from an IMS cartridge.
4445   *
4446   * @param  string   $url    The URL to the cartridge
4447   * @param  stdClass $lti    LTI object
4448   * @throws moodle_exception if the cartridge could not be loaded correctly
4449   * @since Moodle 3.1
4450   */
4451  function lti_load_tool_from_cartridge($url, $lti) {
4452      $toolinfo = lti_load_cartridge($url,
4453          array(
4454              "title" => "name",
4455              "launch_url" => "toolurl",
4456              "secure_launch_url" => "securetoolurl",
4457              "description" => "intro",
4458              "icon" => "icon",
4459              "secure_icon" => "secureicon"
4460          ),
4461          array(
4462              "icon_url" => "extension_icon",
4463              "secure_icon_url" => "extension_secureicon"
4464          )
4465      );
4466      // If an activity name exists, unset the cartridge name so we don't override it.
4467      if (isset($lti->name)) {
4468          unset($toolinfo['name']);
4469      }
4470  
4471      // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4472      if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
4473          $toolinfo['icon'] = $toolinfo['extension_icon'];
4474      }
4475      unset($toolinfo['extension_icon']);
4476  
4477      if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
4478          $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
4479      }
4480      unset($toolinfo['extension_secureicon']);
4481  
4482      foreach ($toolinfo as $property => $value) {
4483          $lti->$property = $value;
4484      }
4485  }
4486  
4487  /**
4488   * Search for a tag within an XML DOMDocument
4489   *
4490   * @param  string $url The url of the cartridge to be loaded
4491   * @param  array  $map The map of tags to keys in the return array
4492   * @param  array  $propertiesmap The map of properties to keys in the return array
4493   * @return array An associative array with the given keys and their values from the cartridge
4494   * @throws moodle_exception if the cartridge could not be loaded correctly
4495   * @since Moodle 3.1
4496   */
4497  function lti_load_cartridge($url, $map, $propertiesmap = array()) {
4498      global $CFG;
4499      require_once($CFG->libdir. "/filelib.php");
4500  
4501      $curl = new curl();
4502      $response = $curl->get($url);
4503  
4504      // Got a completely empty response (real or error), cannot process this with
4505      // DOMDocument::loadXML() because it errors with ValueError. So let's throw
4506      // the moodle_exception before waiting to examine the errors later.
4507      if (trim($response) === '') {
4508          throw new moodle_exception('errorreadingfile', '', '', $url);
4509      }
4510  
4511      // TODO MDL-46023 Replace this code with a call to the new library.
4512      $origerrors = libxml_use_internal_errors(true);
4513      $origentity = lti_libxml_disable_entity_loader(true);
4514      libxml_clear_errors();
4515  
4516      $document = new DOMDocument();
4517      @$document->loadXML($response, LIBXML_NONET);
4518  
4519      $cartridge = new DomXpath($document);
4520  
4521      $errors = libxml_get_errors();
4522  
4523      libxml_clear_errors();
4524      libxml_use_internal_errors($origerrors);
4525      lti_libxml_disable_entity_loader($origentity);
4526  
4527      if (count($errors) > 0) {
4528          $message = 'Failed to load cartridge.';
4529          foreach ($errors as $error) {
4530              $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line;
4531          }
4532          throw new moodle_exception('errorreadingfile', '', '', $url, $message);
4533      }
4534  
4535      $toolinfo = array();
4536      foreach ($map as $tag => $key) {
4537          $value = get_tag($tag, $cartridge);
4538          if ($value) {
4539              $toolinfo[$key] = $value;
4540          }
4541      }
4542      if (!empty($propertiesmap)) {
4543          foreach ($propertiesmap as $property => $key) {
4544              $value = get_tag("property", $cartridge, $property);
4545              if ($value) {
4546                  $toolinfo[$key] = $value;
4547              }
4548          }
4549      }
4550  
4551      return $toolinfo;
4552  }
4553  
4554  /**
4555   * Search for a tag within an XML DOMDocument
4556   *
4557   * @param  stdClass $tagname The name of the tag to search for
4558   * @param  XPath    $xpath   The XML to find the tag in
4559   * @param  XPath    $attribute The attribute to search for (if we should search for a child node with the given
4560   * value for the name attribute
4561   * @since Moodle 3.1
4562   */
4563  function get_tag($tagname, $xpath, $attribute = null) {
4564      if ($attribute) {
4565          $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]');
4566      } else {
4567          $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']');
4568      }
4569      if ($result->length > 0) {
4570          return $result->item(0)->nodeValue;
4571      }
4572      return null;
4573  }
4574  
4575  /**
4576   * Create a new access token.
4577   *
4578   * @param int $typeid Tool type ID
4579   * @param string[] $scopes Scopes permitted for new token
4580   *
4581   * @return stdClass Access token
4582   */
4583  function lti_new_access_token($typeid, $scopes) {
4584      global $DB;
4585  
4586      // Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
4587      $numtries = 0;
4588      do {
4589          $numtries ++;
4590          $generatedtoken = md5(uniqid(rand(), 1));
4591          if ($numtries > 5) {
4592              throw new moodle_exception('Failed to generate LTI access token');
4593          }
4594      } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken)));
4595      $newtoken = new stdClass();
4596      $newtoken->typeid = $typeid;
4597      $newtoken->scope = json_encode(array_values($scopes));
4598      $newtoken->token = $generatedtoken;
4599  
4600      $newtoken->timecreated = time();
4601      $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE;
4602      $newtoken->lastaccess = null;
4603  
4604      $DB->insert_record('lti_access_tokens', $newtoken);
4605  
4606      return $newtoken;
4607  
4608  }
4609  
4610  
4611  /**
4612   * Wrapper for function libxml_disable_entity_loader() deprecated in PHP 8
4613   *
4614   * Method was deprecated in PHP 8 and it shows deprecation message. However it is still
4615   * required in the previous versions on PHP. While Moodle supports both PHP 7 and 8 we need to keep it.
4616   * @see https://php.watch/versions/8.0/libxml_disable_entity_loader-deprecation
4617   *
4618   * @param bool $value
4619   * @return bool
4620   */
4621  function lti_libxml_disable_entity_loader(bool $value): bool {
4622      if (PHP_VERSION_ID < 80000) {
4623          return (bool)libxml_disable_entity_loader($value);
4624      }
4625      return true;
4626  }