Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

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