Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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