Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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