Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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