Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  //
  17  // This file is part of BasicLTI4Moodle
  18  //
  19  // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
  20  // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
  21  // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
  22  // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
  23  // are already supporting or going to support BasicLTI. This project Implements the consumer
  24  // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
  25  // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
  26  // at the GESSI research group at UPC.
  27  // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
  28  // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
  29  // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
  30  //
  31  // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
  32  // of the Universitat Politecnica de Catalunya http://www.upc.edu
  33  // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu.
  34  
  35  /**
  36   * This file contains unit tests for (some of) lti/locallib.php
  37   *
  38   * @package    mod_lti
  39   * @category   phpunit
  40   * @copyright  2009 Marc Alier, Jordi Piguillem, Nikolas Galanis
  41   * @copyright  2009 Universitat Politecnica de Catalunya http://www.upc.edu
  42   * @author     Charles Severance csev@unmich.edu
  43   * @author     Marc Alier (marc.alier@upc.edu)
  44   * @author     Jordi Piguillem
  45   * @author     Nikolas Galanis
  46   * @author     Chris Scribner
  47   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  48   */
  49  namespace mod_lti;
  50  
  51  use mod_lti_external;
  52  use mod_lti_testcase;
  53  
  54  defined('MOODLE_INTERNAL') || die;
  55  
  56  global $CFG;
  57  require_once($CFG->dirroot . '/mod/lti/locallib.php');
  58  require_once($CFG->dirroot . '/mod/lti/servicelib.php');
  59  
  60  /**
  61   * Local library tests
  62   *
  63   * @package    mod_lti
  64   * @copyright  Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com)
  65   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  66   */
  67  class locallib_test extends \advanced_testcase {
  68  
  69      public function test_split_custom_parameters() {
  70          $this->resetAfterTest();
  71  
  72          $tool = new \stdClass();
  73          $tool->enabledcapability = '';
  74          $tool->parameter = '';
  75          $tool->ltiversion = 'LTI-1p0';
  76          $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), "x=1\ny=2", false),
  77              array('custom_x' => '1', 'custom_y' => '2'));
  78  
  79          // Check params with caps.
  80          $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), "X=1", true),
  81              array('custom_x' => '1', 'custom_X' => '1'));
  82  
  83          // Removed repeat of previous test with a semicolon separator.
  84  
  85          $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), 'Review:Chapter=1.2.56', true),
  86              array(
  87                  'custom_review_chapter' => '1.2.56',
  88                  'custom_Review:Chapter' => '1.2.56'));
  89  
  90          $this->assertEquals(lti_split_custom_parameters(null, $tool, array(),
  91              'Complex!@#$^*(){}[]KEY=Complex!@#$^*;(){}[]½Value', true),
  92              array(
  93                  'custom_complex____________key' => 'Complex!@#$^*;(){}[]½Value',
  94                  'custom_Complex!@#$^*(){}[]KEY' => 'Complex!@#$^*;(){}[]½Value'));
  95  
  96          // Test custom parameter that returns $USER property.
  97          $user = $this->getDataGenerator()->create_user(array('middlename' => 'SOMETHING'));
  98          $this->setUser($user);
  99          $this->assertEquals(array('custom_x' => '1', 'custom_y' => 'SOMETHING'),
 100              lti_split_custom_parameters(null, $tool, array(), "x=1\ny=\$Person.name.middle", false));
 101      }
 102  
 103      /**
 104       * This test has been disabled because the test-tool is
 105       * being moved and probably it won't work anymore for this.
 106       * We should be testing here local stuff only and leave
 107       * outside-checks to the conformance tests. MDL-30347
 108       */
 109      public function disabled_test_sign_parameters() {
 110          $correct = array ( 'context_id' => '12345', 'context_label' => 'SI124', 'context_title' => 'Social Computing',
 111              'ext_submit' => 'Click Me', 'lti_message_type' => 'basic-lti-launch-request', 'lti_version' => 'LTI-1p0',
 112              'oauth_consumer_key' => 'lmsng.school.edu', 'oauth_nonce' => '47458148e33a8f9dafb888c3684cf476',
 113              'oauth_signature' => 'qWgaBIezihCbeHgcwUy14tZcyDQ=', 'oauth_signature_method' => 'HMAC-SHA1',
 114              'oauth_timestamp' => '1307141660', 'oauth_version' => '1.0', 'resource_link_id' => '123',
 115              'resource_link_title' => 'Weekly Blog', 'roles' => 'Learner', 'tool_consumer_instance_guid' => 'lmsng.school.edu',
 116              'user_id' => '789');
 117  
 118          $requestparams = array('resource_link_id' => '123', 'resource_link_title' => 'Weekly Blog', 'user_id' => '789',
 119              'roles' => 'Learner', 'context_id' => '12345', 'context_label' => 'SI124', 'context_title' => 'Social Computing');
 120  
 121          $parms = lti_sign_parameters($requestparams, 'http://www.imsglobal.org/developer/LTI/tool.php', 'POST',
 122              'lmsng.school.edu', 'secret', 'Click Me', 'lmsng.school.edu' /*, $org_desc*/);
 123          $this->assertTrue(isset($parms['oauth_nonce']));
 124          $this->assertTrue(isset($parms['oauth_signature']));
 125          $this->assertTrue(isset($parms['oauth_timestamp']));
 126  
 127          // Those things that are hard to mock.
 128          $correct['oauth_nonce'] = $parms['oauth_nonce'];
 129          $correct['oauth_signature'] = $parms['oauth_signature'];
 130          $correct['oauth_timestamp'] = $parms['oauth_timestamp'];
 131          ksort($parms);
 132          ksort($correct);
 133          $this->assertEquals($parms, $correct);
 134      }
 135  
 136      /**
 137       * This test has been disabled because, since its creation,
 138       * the sourceId generation has changed and surely this is outdated.
 139       * Some day these should be replaced by proper tests, but until then
 140       * conformance tests say this is working. MDL-30347
 141       */
 142      public function disabled_test_parse_grade_replace_message() {
 143          $message = '
 144              <imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
 145                <imsx_POXHeader>
 146                  <imsx_POXRequestHeaderInfo>
 147                    <imsx_version>V1.0</imsx_version>
 148                    <imsx_messageIdentifier>999998123</imsx_messageIdentifier>
 149                  </imsx_POXRequestHeaderInfo>
 150                </imsx_POXHeader>
 151                <imsx_POXBody>
 152                  <replaceResultRequest>
 153                    <resultRecord>
 154                      <sourcedGUID>
 155                        <sourcedId>' .
 156              '{&quot;data&quot;:{&quot;instanceid&quot;:&quot;2&quot;,&quot;userid&quot;:&quot;2&quot;},&quot;hash&quot;:' .
 157              '&quot;0b5078feab59b9938c333ceaae21d8e003a7b295e43cdf55338445254421076b&quot;}' .
 158                        '</sourcedId>
 159                      </sourcedGUID>
 160                      <result>
 161                        <resultScore>
 162                          <language>en-us</language>
 163                          <textString>0.92</textString>
 164                        </resultScore>
 165                      </result>
 166                    </resultRecord>
 167                  </replaceResultRequest>
 168                </imsx_POXBody>
 169              </imsx_POXEnvelopeRequest>
 170  ';
 171  
 172          $parsed = lti_parse_grade_replace_message(new SimpleXMLElement($message));
 173  
 174          $this->assertEquals($parsed->userid, '2');
 175          $this->assertEquals($parsed->instanceid, '2');
 176          $this->assertEquals($parsed->sourcedidhash, '0b5078feab59b9938c333ceaae21d8e003a7b295e43cdf55338445254421076b');
 177  
 178          $ltiinstance = (object)array('servicesalt' => '4e5fcc06de1d58.44963230');
 179  
 180          lti_verify_sourcedid($ltiinstance, $parsed);
 181      }
 182  
 183      public function test_lti_ensure_url_is_https() {
 184          $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('http://moodle.org'));
 185          $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('moodle.org'));
 186          $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('https://moodle.org'));
 187      }
 188  
 189      /**
 190       * Test lti_get_url_thumbprint against various URLs
 191       */
 192      public function test_lti_get_url_thumbprint() {
 193          // Note: trailing and double slash are expected right now.  Must evaluate if it must be removed at some point.
 194          $this->assertEquals('moodle.org/', lti_get_url_thumbprint('http://MOODLE.ORG'));
 195          $this->assertEquals('moodle.org/', lti_get_url_thumbprint('http://www.moodle.org'));
 196          $this->assertEquals('moodle.org/', lti_get_url_thumbprint('https://www.moodle.org'));
 197          $this->assertEquals('moodle.org/', lti_get_url_thumbprint('moodle.org'));
 198          $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('http://moodle.org/this/is/moodle'));
 199          $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('https://moodle.org/this/is/moodle'));
 200          $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('moodle.org/this/is/moodle'));
 201          $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('moodle.org/this/is/moodle?'));
 202          $this->assertEquals('moodle.org//this/is/moodle?foo=bar', lti_get_url_thumbprint('moodle.org/this/is/moodle?foo=bar'));
 203      }
 204  
 205      /*
 206       * Verify that lti_build_request does handle resource_link_id as expected
 207       */
 208      public function test_lti_buid_request_resource_link_id() {
 209          $this->resetAfterTest();
 210  
 211          self::setUser($this->getDataGenerator()->create_user());
 212          $course   = $this->getDataGenerator()->create_course();
 213          $instance = $this->getDataGenerator()->create_module('lti', array(
 214              'intro'       => "<p>This</p>\nhas\r\n<p>some</p>\nnew\n\rlines",
 215              'introformat' => FORMAT_HTML,
 216              'course'      => $course->id,
 217          ));
 218  
 219          $typeconfig = array(
 220              'acceptgrades'     => 1,
 221              'forcessl'         => 0,
 222              'sendname'         => 2,
 223              'sendemailaddr'    => 2,
 224              'customparameters' => '',
 225          );
 226  
 227          // Normal call, we expect $instance->id to be used as resource_link_id.
 228          $params = lti_build_request($instance, $typeconfig, $course, null);
 229          $this->assertSame($instance->id, $params['resource_link_id']);
 230  
 231          // If there is a resource_link_id set, it gets precedence.
 232          $instance->resource_link_id = $instance->id + 99;
 233          $params = lti_build_request($instance, $typeconfig, $course, null);
 234          $this->assertSame($instance->resource_link_id, $params['resource_link_id']);
 235  
 236          // With none set, resource_link_id is not set either.
 237          unset($instance->id);
 238          unset($instance->resource_link_id);
 239          $params = lti_build_request($instance, $typeconfig, $course, null);
 240          $this->assertArrayNotHasKey('resource_link_id', $params);
 241      }
 242  
 243      /**
 244       * Test lti_build_request's resource_link_description and ensure
 245       * that the newlines in the description are correct.
 246       */
 247      public function test_lti_build_request_description() {
 248          $this->resetAfterTest();
 249  
 250          self::setUser($this->getDataGenerator()->create_user());
 251          $course   = $this->getDataGenerator()->create_course();
 252          $instance = $this->getDataGenerator()->create_module('lti', array(
 253              'intro'       => "<p>This</p>\nhas\r\n<p>some</p>\nnew\n\rlines",
 254              'introformat' => FORMAT_HTML,
 255              'course'      => $course->id,
 256          ));
 257  
 258          $typeconfig = array(
 259              'acceptgrades'     => 1,
 260              'forcessl'         => 0,
 261              'sendname'         => 2,
 262              'sendemailaddr'    => 2,
 263              'customparameters' => '',
 264          );
 265  
 266          $params = lti_build_request($instance, $typeconfig, $course, null);
 267  
 268          $ncount = substr_count($params['resource_link_description'], "\n");
 269          $this->assertGreaterThan(0, $ncount);
 270  
 271          $rcount = substr_count($params['resource_link_description'], "\r");
 272          $this->assertGreaterThan(0, $rcount);
 273  
 274          $this->assertEquals($ncount, $rcount, 'The number of \n characters should be the same as the number of \r characters');
 275  
 276          $rncount = substr_count($params['resource_link_description'], "\r\n");
 277          $this->assertGreaterThan(0, $rncount);
 278  
 279          $this->assertEquals($ncount, $rncount, 'All newline characters should be a combination of \r\n');
 280      }
 281  
 282      /**
 283       * Tests lti_prepare_type_for_save's handling of the "Force SSL" configuration.
 284       */
 285      public function test_lti_prepare_type_for_save_forcessl() {
 286          $type = new \stdClass();
 287          $config = new \stdClass();
 288  
 289          // Try when the forcessl config property is not set.
 290          lti_prepare_type_for_save($type, $config);
 291          $this->assertObjectHasAttribute('lti_forcessl', $config);
 292          $this->assertEquals(0, $config->lti_forcessl);
 293          $this->assertEquals(0, $type->forcessl);
 294  
 295          // Try when forcessl config property is set.
 296          $config->lti_forcessl = 1;
 297          lti_prepare_type_for_save($type, $config);
 298          $this->assertObjectHasAttribute('lti_forcessl', $config);
 299          $this->assertEquals(1, $config->lti_forcessl);
 300          $this->assertEquals(1, $type->forcessl);
 301  
 302          // Try when forcessl config property is set to 0.
 303          $config->lti_forcessl = 0;
 304          lti_prepare_type_for_save($type, $config);
 305          $this->assertObjectHasAttribute('lti_forcessl', $config);
 306          $this->assertEquals(0, $config->lti_forcessl);
 307          $this->assertEquals(0, $type->forcessl);
 308      }
 309  
 310      /**
 311       * Tests lti_load_type_from_cartridge and lti_load_type_if_cartridge
 312       */
 313      public function test_lti_load_type_from_cartridge() {
 314          $type = new \stdClass();
 315          $type->lti_toolurl = $this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml');
 316  
 317          lti_load_type_if_cartridge($type);
 318  
 319          $this->assertEquals('Example tool', $type->lti_typename);
 320          $this->assertEquals('Example tool description', $type->lti_description);
 321          $this->assertEquals('http://www.example.com/lti/provider.php', $type->lti_toolurl);
 322          $this->assertEquals('http://download.moodle.org/unittest/test.jpg', $type->lti_icon);
 323          $this->assertEquals('https://download.moodle.org/unittest/test.jpg', $type->lti_secureicon);
 324      }
 325  
 326      /**
 327       * Tests lti_load_tool_from_cartridge and lti_load_tool_if_cartridge
 328       */
 329      public function test_lti_load_tool_from_cartridge() {
 330          $lti = new \stdClass();
 331          $lti->toolurl = $this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml');
 332  
 333          lti_load_tool_if_cartridge($lti);
 334  
 335          $this->assertEquals('Example tool', $lti->name);
 336          $this->assertEquals('Example tool description', $lti->intro);
 337          $this->assertEquals('http://www.example.com/lti/provider.php', $lti->toolurl);
 338          $this->assertEquals('https://www.example.com/lti/provider.php', $lti->securetoolurl);
 339          $this->assertEquals('http://download.moodle.org/unittest/test.jpg', $lti->icon);
 340          $this->assertEquals('https://download.moodle.org/unittest/test.jpg', $lti->secureicon);
 341      }
 342  
 343      /**
 344       * Tests for lti_build_content_item_selection_request().
 345       */
 346      public function test_lti_build_content_item_selection_request() {
 347          $this->resetAfterTest();
 348  
 349          $this->setAdminUser();
 350          // Create a tool proxy.
 351          $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array());
 352  
 353          // Create a tool type, associated with that proxy.
 354          $type = new \stdClass();
 355          $data = new \stdClass();
 356          $data->lti_contentitem = true;
 357          $type->state = LTI_TOOL_STATE_CONFIGURED;
 358          $type->name = "Test tool";
 359          $type->description = "Example description";
 360          $type->toolproxyid = $proxy->id;
 361          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
 362  
 363          $typeid = lti_add_type($type, $data);
 364  
 365          $typeconfig = lti_get_type_config($typeid);
 366  
 367          $course = $this->getDataGenerator()->create_course();
 368          $returnurl = new \moodle_url('/');
 369  
 370          // Default parameters.
 371          $result = lti_build_content_item_selection_request($typeid, $course, $returnurl);
 372          $this->assertNotEmpty($result);
 373          $this->assertNotEmpty($result->params);
 374          $this->assertNotEmpty($result->url);
 375          $params = $result->params;
 376          $url = $result->url;
 377          $this->assertEquals($typeconfig['toolurl'], $url);
 378          $this->assertEquals('ContentItemSelectionRequest', $params['lti_message_type']);
 379          $this->assertEquals(LTI_VERSION_1, $params['lti_version']);
 380          $this->assertEquals('application/vnd.ims.lti.v1.ltilink', $params['accept_media_types']);
 381          $this->assertEquals('frame,iframe,window', $params['accept_presentation_document_targets']);
 382          $this->assertEquals($returnurl->out(false), $params['content_item_return_url']);
 383          $this->assertEquals('false', $params['accept_unsigned']);
 384          $this->assertEquals('true', $params['accept_multiple']);
 385          $this->assertEquals('false', $params['accept_copy_advice']);
 386          $this->assertEquals('false', $params['auto_create']);
 387          $this->assertEquals($type->name, $params['title']);
 388          $this->assertFalse(isset($params['resource_link_id']));
 389          $this->assertFalse(isset($params['resource_link_title']));
 390          $this->assertFalse(isset($params['resource_link_description']));
 391          $this->assertFalse(isset($params['launch_presentation_return_url']));
 392          $this->assertFalse(isset($params['lis_result_sourcedid']));
 393          $this->assertEquals($params['tool_consumer_instance_guid'], 'www.example.com');
 394  
 395          // Custom parameters.
 396          $title = 'My custom title';
 397          $text = 'This is the tool description';
 398          $mediatypes = ['image/*', 'video/*'];
 399          $targets = ['embed', 'iframe'];
 400          $result = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, $mediatypes, $targets,
 401              true, true, true, true, true);
 402          $this->assertNotEmpty($result);
 403          $this->assertNotEmpty($result->params);
 404          $this->assertNotEmpty($result->url);
 405          $params = $result->params;
 406          $this->assertEquals(implode(',', $mediatypes), $params['accept_media_types']);
 407          $this->assertEquals(implode(',', $targets), $params['accept_presentation_document_targets']);
 408          $this->assertEquals('true', $params['accept_unsigned']);
 409          $this->assertEquals('true', $params['accept_multiple']);
 410          $this->assertEquals('true', $params['accept_copy_advice']);
 411          $this->assertEquals('true', $params['auto_create']);
 412          $this->assertEquals($title, $params['title']);
 413          $this->assertEquals($text, $params['text']);
 414  
 415          // Invalid flag values.
 416          $result = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, $mediatypes, $targets,
 417              'aa', -1, 0, 1, 0xabc);
 418          $this->assertNotEmpty($result);
 419          $this->assertNotEmpty($result->params);
 420          $this->assertNotEmpty($result->url);
 421          $params = $result->params;
 422          $this->assertEquals(implode(',', $mediatypes), $params['accept_media_types']);
 423          $this->assertEquals(implode(',', $targets), $params['accept_presentation_document_targets']);
 424          $this->assertEquals('false', $params['accept_unsigned']);
 425          $this->assertEquals('false', $params['accept_multiple']);
 426          $this->assertEquals('false', $params['accept_copy_advice']);
 427          $this->assertEquals('false', $params['auto_create']);
 428          $this->assertEquals($title, $params['title']);
 429          $this->assertEquals($text, $params['text']);
 430      }
 431  
 432      /**
 433       * Test for lti_build_content_item_selection_request() with nonexistent tool type ID parameter.
 434       */
 435      public function test_lti_build_content_item_selection_request_invalid_tooltype() {
 436          $this->resetAfterTest();
 437  
 438          $this->setAdminUser();
 439          $course = $this->getDataGenerator()->create_course();
 440          $returnurl = new \moodle_url('/');
 441  
 442          // Should throw Exception on non-existent tool type.
 443          $this->expectException('moodle_exception');
 444          lti_build_content_item_selection_request(1, $course, $returnurl);
 445      }
 446  
 447      /**
 448       * Test for lti_build_content_item_selection_request() with invalid media types parameter.
 449       */
 450      public function test_lti_build_content_item_selection_request_invalid_mediatypes() {
 451          $this->resetAfterTest();
 452  
 453          $this->setAdminUser();
 454  
 455          // Create a tool type, associated with that proxy.
 456          $type = new \stdClass();
 457          $data = new \stdClass();
 458          $data->lti_contentitem = true;
 459          $type->state = LTI_TOOL_STATE_CONFIGURED;
 460          $type->name = "Test tool";
 461          $type->description = "Example description";
 462          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
 463  
 464          $typeid = lti_add_type($type, $data);
 465          $course = $this->getDataGenerator()->create_course();
 466          $returnurl = new \moodle_url('/');
 467  
 468          // Should throw coding_exception on non-array media types.
 469          $mediatypes = 'image/*,video/*';
 470          $this->expectException('coding_exception');
 471          lti_build_content_item_selection_request($typeid, $course, $returnurl, '', '', $mediatypes);
 472      }
 473  
 474      /**
 475       * Test for lti_build_content_item_selection_request() with invalid presentation targets parameter.
 476       */
 477      public function test_lti_build_content_item_selection_request_invalid_presentationtargets() {
 478          $this->resetAfterTest();
 479  
 480          $this->setAdminUser();
 481  
 482          // Create a tool type, associated with that proxy.
 483          $type = new \stdClass();
 484          $data = new \stdClass();
 485          $data->lti_contentitem = true;
 486          $type->state = LTI_TOOL_STATE_CONFIGURED;
 487          $type->name = "Test tool";
 488          $type->description = "Example description";
 489          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
 490  
 491          $typeid = lti_add_type($type, $data);
 492          $course = $this->getDataGenerator()->create_course();
 493          $returnurl = new \moodle_url('/');
 494  
 495          // Should throw coding_exception on non-array presentation targets.
 496          $targets = 'frame,iframe';
 497          $this->expectException('coding_exception');
 498          lti_build_content_item_selection_request($typeid, $course, $returnurl, '', '', [], $targets);
 499      }
 500  
 501      /**
 502       * Provider for test_lti_get_best_tool_by_url.
 503       *
 504       * @return array of [urlToTest, expectedTool, allTools]
 505       */
 506      public function lti_get_best_tool_by_url_provider() {
 507          $tools = [
 508              (object) [
 509                  'name' => 'Here',
 510                  'baseurl' => 'https://example.com/i/am/?where=here',
 511                  'tooldomain' => 'example.com',
 512                  'state' => LTI_TOOL_STATE_CONFIGURED,
 513                  'course' => SITEID
 514              ],
 515              (object) [
 516                  'name' => 'There',
 517                  'baseurl' => 'https://example.com/i/am/?where=there',
 518                  'tooldomain' => 'example.com',
 519                  'state' => LTI_TOOL_STATE_CONFIGURED,
 520                  'course' => SITEID
 521              ],
 522              (object) [
 523                  'name' => 'Not here',
 524                  'baseurl' => 'https://example.com/i/am/?where=not/here',
 525                  'tooldomain' => 'example.com',
 526                  'state' => LTI_TOOL_STATE_CONFIGURED,
 527                  'course' => SITEID
 528              ],
 529              (object) [
 530                  'name' => 'Here',
 531                  'baseurl' => 'https://example.com/i/am/',
 532                  'tooldomain' => 'example.com',
 533                  'state' => LTI_TOOL_STATE_CONFIGURED,
 534                  'course' => SITEID
 535              ],
 536              (object) [
 537                  'name' => 'Here',
 538                  'baseurl' => 'https://example.com/i/was',
 539                  'tooldomain' => 'example.com',
 540                  'state' => LTI_TOOL_STATE_CONFIGURED,
 541                  'course' => SITEID
 542              ],
 543              (object) [
 544                  'name' => 'Here',
 545                  'baseurl' => 'https://badexample.com/i/am/?where=here',
 546                  'tooldomain' => 'badexample.com',
 547                  'state' => LTI_TOOL_STATE_CONFIGURED,
 548                  'course' => SITEID
 549              ],
 550          ];
 551  
 552          $data = [
 553              [
 554                  'url' => $tools[0]->baseurl,
 555                  'expected' => $tools[0],
 556              ],
 557              [
 558                  'url' => $tools[1]->baseurl,
 559                  'expected' => $tools[1],
 560              ],
 561              [
 562                  'url' => $tools[2]->baseurl,
 563                  'expected' => $tools[2],
 564              ],
 565              [
 566                  'url' => $tools[3]->baseurl,
 567                  'expected' => $tools[3],
 568              ],
 569              [
 570                  'url' => $tools[4]->baseurl,
 571                  'expected' => $tools[4],
 572              ],
 573              [
 574                  'url' => $tools[5]->baseurl,
 575                  'expected' => $tools[5],
 576              ],
 577              [
 578                  'url' => 'https://nomatch.com/i/am/',
 579                  'expected' => null
 580              ],
 581              [
 582                  'url' => 'https://example.com',
 583                  'expected' => null
 584              ],
 585              [
 586                  'url' => 'https://example.com/i/am/?where=unknown',
 587                  'expected' => $tools[3]
 588              ]
 589          ];
 590  
 591          // Construct the final array as required by the provider API. Each row
 592          // of the array contains the URL to test, the expected tool, and
 593          // the complete list of tools.
 594          return array_map(function($data) use ($tools) {
 595              return [$data['url'], $data['expected'], $tools];
 596          }, $data);
 597      }
 598  
 599      /**
 600       * Test lti_get_best_tool_by_url.
 601       *
 602       * @dataProvider lti_get_best_tool_by_url_provider
 603       * @param string $url The URL to test.
 604       * @param object $expected The expected tool matching the URL.
 605       * @param array $tools The pool of tools to match the URL with.
 606       */
 607      public function test_lti_get_best_tool_by_url($url, $expected, $tools) {
 608          $actual = lti_get_best_tool_by_url($url, $tools, null);
 609          $this->assertSame($expected, $actual);
 610      }
 611  
 612      /**
 613       * Test lti_get_jwt_message_type_mapping().
 614       */
 615      public function test_lti_get_jwt_message_type_mapping() {
 616          $mapping = [
 617              'basic-lti-launch-request' => 'LtiResourceLinkRequest',
 618              'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest',
 619              'LtiDeepLinkingResponse' => 'ContentItemSelection',
 620          ];
 621  
 622          $this->assertEquals($mapping, lti_get_jwt_message_type_mapping());
 623      }
 624  
 625      /**
 626       * Test lti_get_jwt_claim_mapping()
 627       */
 628      public function test_lti_get_jwt_claim_mapping() {
 629          $mapping = [
 630              'accept_copy_advice' => [
 631                  'suffix' => 'dl',
 632                  'group' => 'deep_linking_settings',
 633                  'claim' => 'accept_copy_advice',
 634                  'isarray' => false,
 635                  'type' => 'boolean'
 636              ],
 637              'accept_media_types' => [
 638                  'suffix' => 'dl',
 639                  'group' => 'deep_linking_settings',
 640                  'claim' => 'accept_media_types',
 641                  'isarray' => true
 642              ],
 643              'accept_multiple' => [
 644                  'suffix' => 'dl',
 645                  'group' => 'deep_linking_settings',
 646                  'claim' => 'accept_multiple',
 647                  'isarray' => false,
 648                  'type' => 'boolean'
 649              ],
 650              'accept_presentation_document_targets' => [
 651                  'suffix' => 'dl',
 652                  'group' => 'deep_linking_settings',
 653                  'claim' => 'accept_presentation_document_targets',
 654                  'isarray' => true
 655              ],
 656              'accept_types' => [
 657                  'suffix' => 'dl',
 658                  'group' => 'deep_linking_settings',
 659                  'claim' => 'accept_types',
 660                  'isarray' => true
 661              ],
 662              'accept_unsigned' => [
 663                  'suffix' => 'dl',
 664                  'group' => 'deep_linking_settings',
 665                  'claim' => 'accept_unsigned',
 666                  'isarray' => false,
 667                  'type' => 'boolean'
 668              ],
 669              'auto_create' => [
 670                  'suffix' => 'dl',
 671                  'group' => 'deep_linking_settings',
 672                  'claim' => 'auto_create',
 673                  'isarray' => false,
 674                  'type' => 'boolean'
 675              ],
 676              'can_confirm' => [
 677                  'suffix' => 'dl',
 678                  'group' => 'deep_linking_settings',
 679                  'claim' => 'can_confirm',
 680                  'isarray' => false,
 681                  'type' => 'boolean'
 682              ],
 683              'content_item_return_url' => [
 684                  'suffix' => 'dl',
 685                  'group' => 'deep_linking_settings',
 686                  'claim' => 'deep_link_return_url',
 687                  'isarray' => false
 688              ],
 689              'content_items' => [
 690                  'suffix' => 'dl',
 691                  'group' => '',
 692                  'claim' => 'content_items',
 693                  'isarray' => true
 694              ],
 695              'data' => [
 696                  'suffix' => 'dl',
 697                  'group' => 'deep_linking_settings',
 698                  'claim' => 'data',
 699                  'isarray' => false
 700              ],
 701              'text' => [
 702                  'suffix' => 'dl',
 703                  'group' => 'deep_linking_settings',
 704                  'claim' => 'text',
 705                  'isarray' => false
 706              ],
 707              'title' => [
 708                  'suffix' => 'dl',
 709                  'group' => 'deep_linking_settings',
 710                  'claim' => 'title',
 711                  'isarray' => false
 712              ],
 713              'lti_msg' => [
 714                  'suffix' => 'dl',
 715                  'group' => '',
 716                  'claim' => 'msg',
 717                  'isarray' => false
 718              ],
 719              'lti_log' => [
 720                  'suffix' => 'dl',
 721                  'group' => '',
 722                  'claim' => 'log',
 723                  'isarray' => false
 724              ],
 725              'lti_errormsg' => [
 726                  'suffix' => 'dl',
 727                  'group' => '',
 728                  'claim' => 'errormsg',
 729                  'isarray' => false
 730              ],
 731              'lti_errorlog' => [
 732                  'suffix' => 'dl',
 733                  'group' => '',
 734                  'claim' => 'errorlog',
 735                  'isarray' => false
 736              ],
 737              'context_id' => [
 738                  'suffix' => '',
 739                  'group' => 'context',
 740                  'claim' => 'id',
 741                  'isarray' => false
 742              ],
 743              'context_label' => [
 744                  'suffix' => '',
 745                  'group' => 'context',
 746                  'claim' => 'label',
 747                  'isarray' => false
 748              ],
 749              'context_title' => [
 750                  'suffix' => '',
 751                  'group' => 'context',
 752                  'claim' => 'title',
 753                  'isarray' => false
 754              ],
 755              'context_type' => [
 756                  'suffix' => '',
 757                  'group' => 'context',
 758                  'claim' => 'type',
 759                  'isarray' => true
 760              ],
 761              'lis_course_offering_sourcedid' => [
 762                  'suffix' => '',
 763                  'group' => 'lis',
 764                  'claim' => 'course_offering_sourcedid',
 765                  'isarray' => false
 766              ],
 767              'lis_course_section_sourcedid' => [
 768                  'suffix' => '',
 769                  'group' => 'lis',
 770                  'claim' => 'course_section_sourcedid',
 771                  'isarray' => false
 772              ],
 773              'launch_presentation_css_url' => [
 774                  'suffix' => '',
 775                  'group' => 'launch_presentation',
 776                  'claim' => 'css_url',
 777                  'isarray' => false
 778              ],
 779              'launch_presentation_document_target' => [
 780                  'suffix' => '',
 781                  'group' => 'launch_presentation',
 782                  'claim' => 'document_target',
 783                  'isarray' => false
 784              ],
 785              'launch_presentation_height' => [
 786                  'suffix' => '',
 787                  'group' => 'launch_presentation',
 788                  'claim' => 'height',
 789                  'isarray' => false
 790              ],
 791              'launch_presentation_locale' => [
 792                  'suffix' => '',
 793                  'group' => 'launch_presentation',
 794                  'claim' => 'locale',
 795                  'isarray' => false
 796              ],
 797              'launch_presentation_return_url' => [
 798                  'suffix' => '',
 799                  'group' => 'launch_presentation',
 800                  'claim' => 'return_url',
 801                  'isarray' => false
 802              ],
 803              'launch_presentation_width' => [
 804                  'suffix' => '',
 805                  'group' => 'launch_presentation',
 806                  'claim' => 'width',
 807                  'isarray' => false
 808              ],
 809              'lis_person_contact_email_primary' => [
 810                  'suffix' => '',
 811                  'group' => null,
 812                  'claim' => 'email',
 813                  'isarray' => false
 814              ],
 815              'lis_person_name_family' => [
 816                  'suffix' => '',
 817                  'group' => null,
 818                  'claim' => 'family_name',
 819                  'isarray' => false
 820              ],
 821              'lis_person_name_full' => [
 822                  'suffix' => '',
 823                  'group' => null,
 824                  'claim' => 'name',
 825                  'isarray' => false
 826              ],
 827              'lis_person_name_given' => [
 828                  'suffix' => '',
 829                  'group' => null,
 830                  'claim' => 'given_name',
 831                  'isarray' => false
 832              ],
 833              'lis_person_sourcedid' => [
 834                  'suffix' => '',
 835                  'group' => 'lis',
 836                  'claim' => 'person_sourcedid',
 837                  'isarray' => false
 838              ],
 839              'user_id' => [
 840                  'suffix' => '',
 841                  'group' => null,
 842                  'claim' => 'sub',
 843                  'isarray' => false
 844              ],
 845              'user_image' => [
 846                  'suffix' => '',
 847                  'group' => null,
 848                  'claim' => 'picture',
 849                  'isarray' => false
 850              ],
 851              'roles' => [
 852                  'suffix' => '',
 853                  'group' => '',
 854                  'claim' => 'roles',
 855                  'isarray' => true
 856              ],
 857              'role_scope_mentor' => [
 858                  'suffix' => '',
 859                  'group' => '',
 860                  'claim' => 'role_scope_mentor',
 861                  'isarray' => false
 862              ],
 863              'deployment_id' => [
 864                  'suffix' => '',
 865                  'group' => '',
 866                  'claim' => 'deployment_id',
 867                  'isarray' => false
 868              ],
 869              'lti_message_type' => [
 870                  'suffix' => '',
 871                  'group' => '',
 872                  'claim' => 'message_type',
 873                  'isarray' => false
 874              ],
 875              'lti_version' => [
 876                  'suffix' => '',
 877                  'group' => '',
 878                  'claim' => 'version',
 879                  'isarray' => false
 880              ],
 881              'resource_link_description' => [
 882                  'suffix' => '',
 883                  'group' => 'resource_link',
 884                  'claim' => 'description',
 885                  'isarray' => false
 886              ],
 887              'resource_link_id' => [
 888                  'suffix' => '',
 889                  'group' => 'resource_link',
 890                  'claim' => 'id',
 891                  'isarray' => false
 892              ],
 893              'resource_link_title' => [
 894                  'suffix' => '',
 895                  'group' => 'resource_link',
 896                  'claim' => 'title',
 897                  'isarray' => false
 898              ],
 899              'tool_consumer_info_product_family_code' => [
 900                  'suffix' => '',
 901                  'group' => 'tool_platform',
 902                  'claim' => 'product_family_code',
 903                  'isarray' => false
 904              ],
 905              'tool_consumer_info_version' => [
 906                  'suffix' => '',
 907                  'group' => 'tool_platform',
 908                  'claim' => 'version',
 909                  'isarray' => false
 910              ],
 911              'tool_consumer_instance_contact_email' => [
 912                  'suffix' => '',
 913                  'group' => 'tool_platform',
 914                  'claim' => 'contact_email',
 915                  'isarray' => false
 916              ],
 917              'tool_consumer_instance_description' => [
 918                  'suffix' => '',
 919                  'group' => 'tool_platform',
 920                  'claim' => 'description',
 921                  'isarray' => false
 922              ],
 923              'tool_consumer_instance_guid' => [
 924                  'suffix' => '',
 925                  'group' => 'tool_platform',
 926                  'claim' => 'guid',
 927                  'isarray' => false
 928              ],
 929              'tool_consumer_instance_name' => [
 930                  'suffix' => '',
 931                  'group' => 'tool_platform',
 932                  'claim' => 'name',
 933                  'isarray' => false
 934              ],
 935              'tool_consumer_instance_url' => [
 936                  'suffix' => '',
 937                  'group' => 'tool_platform',
 938                  'claim' => 'url',
 939                  'isarray' => false
 940              ],
 941              'custom_context_memberships_v2_url' => [
 942                  'suffix' => 'nrps',
 943                  'group' => 'namesroleservice',
 944                  'claim' => 'context_memberships_url',
 945                  'isarray' => false
 946              ],
 947              'custom_context_memberships_versions' => [
 948                  'suffix' => 'nrps',
 949                  'group' => 'namesroleservice',
 950                  'claim' => 'service_versions',
 951                  'isarray' => true
 952              ],
 953              'custom_gradebookservices_scope' => [
 954                  'suffix' => 'ags',
 955                  'group' => 'endpoint',
 956                  'claim' => 'scope',
 957                  'isarray' => true
 958              ],
 959              'custom_lineitems_url' => [
 960                  'suffix' => 'ags',
 961                  'group' => 'endpoint',
 962                  'claim' => 'lineitems',
 963                  'isarray' => false
 964              ],
 965              'custom_lineitem_url' => [
 966                  'suffix' => 'ags',
 967                  'group' => 'endpoint',
 968                  'claim' => 'lineitem',
 969                  'isarray' => false
 970              ],
 971              'custom_results_url' => [
 972                  'suffix' => 'ags',
 973                  'group' => 'endpoint',
 974                  'claim' => 'results',
 975                  'isarray' => false
 976              ],
 977              'custom_result_url' => [
 978                  'suffix' => 'ags',
 979                  'group' => 'endpoint',
 980                  'claim' => 'result',
 981                  'isarray' => false
 982              ],
 983              'custom_scores_url' => [
 984                  'suffix' => 'ags',
 985                  'group' => 'endpoint',
 986                  'claim' => 'scores',
 987                  'isarray' => false
 988              ],
 989              'custom_score_url' => [
 990                  'suffix' => 'ags',
 991                  'group' => 'endpoint',
 992                  'claim' => 'score',
 993                  'isarray' => false
 994              ],
 995              'lis_outcome_service_url' => [
 996                  'suffix' => 'bo',
 997                  'group' => 'basicoutcome',
 998                  'claim' => 'lis_outcome_service_url',
 999                  'isarray' => false
1000              ],
1001              'lis_result_sourcedid' => [
1002                  'suffix' => 'bo',
1003                  'group' => 'basicoutcome',
1004                  'claim' => 'lis_result_sourcedid',
1005                  'isarray' => false
1006              ],
1007          ];
1008  
1009          $this->assertEquals($mapping, lti_get_jwt_claim_mapping());
1010      }
1011  
1012      /**
1013       * Test lti_build_standard_message().
1014       */
1015      public function test_lti_build_standard_message_institution_name_set() {
1016          global $CFG;
1017  
1018          $this->resetAfterTest();
1019  
1020          $CFG->mod_lti_institution_name = 'some institution name lols';
1021  
1022          $course   = $this->getDataGenerator()->create_course();
1023          $instance = $this->getDataGenerator()->create_module('lti',
1024              [
1025                  'course' => $course->id,
1026              ]
1027          );
1028  
1029          $message = lti_build_standard_message($instance, '2', LTI_VERSION_1);
1030  
1031          $this->assertEquals('moodle-2', $message['ext_lms']);
1032          $this->assertEquals('moodle', $message['tool_consumer_info_product_family_code']);
1033          $this->assertEquals(LTI_VERSION_1, $message['lti_version']);
1034          $this->assertEquals('basic-lti-launch-request', $message['lti_message_type']);
1035          $this->assertEquals('2', $message['tool_consumer_instance_guid']);
1036          $this->assertEquals('some institution name lols', $message['tool_consumer_instance_name']);
1037          $this->assertEquals('PHPUnit test site', $message['tool_consumer_instance_description']);
1038      }
1039  
1040      /**
1041       * Test lti_build_standard_message().
1042       */
1043      public function test_lti_build_standard_message_institution_name_not_set() {
1044          $this->resetAfterTest();
1045  
1046          $course   = $this->getDataGenerator()->create_course();
1047          $instance = $this->getDataGenerator()->create_module('lti',
1048              [
1049                  'course' => $course->id,
1050              ]
1051          );
1052  
1053          $message = lti_build_standard_message($instance, '2', LTI_VERSION_2);
1054  
1055          $this->assertEquals('moodle-2', $message['ext_lms']);
1056          $this->assertEquals('moodle', $message['tool_consumer_info_product_family_code']);
1057          $this->assertEquals(LTI_VERSION_2, $message['lti_version']);
1058          $this->assertEquals('basic-lti-launch-request', $message['lti_message_type']);
1059          $this->assertEquals('2', $message['tool_consumer_instance_guid']);
1060          $this->assertEquals('phpunit', $message['tool_consumer_instance_name']);
1061          $this->assertEquals('PHPUnit test site', $message['tool_consumer_instance_description']);
1062      }
1063  
1064      /**
1065       * Test lti_verify_jwt_signature().
1066       */
1067      public function test_lti_verify_jwt_signature() {
1068          $this->resetAfterTest();
1069  
1070          $this->setAdminUser();
1071  
1072          // Create a tool type, associated with that proxy.
1073          $type = new \stdClass();
1074          $type->state = LTI_TOOL_STATE_CONFIGURED;
1075          $type->name = "Test tool";
1076          $type->description = "Example description";
1077          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1078  
1079          $config = new \stdClass();
1080          $config->lti_publickey = '-----BEGIN PUBLIC KEY-----
1081  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
1082  vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
1083  aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
1084  tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
1085  e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
1086  V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
1087  MwIDAQAB
1088  -----END PUBLIC KEY-----';
1089  
1090          $config->lti_keytype = LTI_RSA_KEY;
1091  
1092          $typeid = lti_add_type($type, $config);
1093  
1094          lti_verify_jwt_signature($typeid, '', 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4g' .
1095              'RG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOs' .
1096              'S_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMgu' .
1097              'EIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iY' .
1098              'v7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA');
1099      }
1100  
1101      /**
1102       * Test lti_verify_jwt_signature_jwk().
1103       */
1104      public function test_lti_verify_jwt_signature_jwk() {
1105          $this->resetAfterTest();
1106  
1107          $this->setAdminUser();
1108  
1109          // Create a tool type, associated with that proxy.
1110          $type = new \stdClass();
1111          $type->state = LTI_TOOL_STATE_CONFIGURED;
1112          $type->name = "Test tool";
1113          $type->description = "Example description";
1114          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1115  
1116          $config = new \stdClass();
1117          $config->lti_publickeyset = dirname(__FILE__) . '/fixtures/test_keyset';
1118  
1119          $config->lti_keytype = LTI_JWK_KEYSET;
1120  
1121          $typeid = lti_add_type($type, $config);
1122  
1123          $jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjU3YzExNzdkMmQ1M2EwMjFjNzM';
1124          $jwt .= '3NTY0OTFjMTM3YjE3In0.eyJpc3MiOiJnclJvbkd3RTd1WjRwZ28iLCJzdWIiOiJnclJvb';
1125          $jwt .= 'kd3RTd1WjRwZ28iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0L21vb2RsZS9tb2QvbHRpL3R';
1126          $jwt .= 'va2VuLnBocCIsImp0aSI6IjFlMUJPVEczVFJjbFdUem00dERsMGc9PSIsImlhdCI6MTU4M';
1127          $jwt .= 'Dg1NTUwNX0.Lowhc9ovNAXRb2rkAnv1oozDXlRD54Mz2JS1i8Zx4yGWQzmXzam-La19_g0';
1128          $jwt .= 'CTnwlKM6gxaInnRKFRAcwhJVcWec389liLAjMbna6d6iTWYTZr7q_4BIe3CT_oTMWASGta';
1129          $jwt .= 'Paaq53ch1rO4YdueEtmtd1K47ibo4Lhu1jmP_icc3lxjfnqiv4vIYdy7W2JQEzpk1ImuQr';
1130          $jwt .= 'AlO1xR3fZ6bgcJhVIaw5xoaZD3ZgEjuZOQXMkywv1bL-mL17RX336CzHd8rYZg82QXrBzb';
1131          $jwt .= 'NWzAlaZxv9VSug8t6mORvM6TkYYWjqEBKemgkD5rNh1BHrPcjWP7vy2Jz7YMjLsmuvDuLK';
1132          $jwt .= '_PHYIKL--s4gcXWoYmOu1vj-SgoPczTJPoiBD35hAKqVHy5ggHaYHBy95_bbcFd8H1smHw';
1133          $jwt .= 'pejrAFj1QAwGyTISLzUm08oq7Ak0tSxRKKXw4lpZAka1MmYxO3tJ_3-MXw6Bwz12bNgitJ';
1134          $jwt .= 'lQd6n3kkGLCJAmANeRkPsH6eZVwF0n2cjh2O1JAwyNcMD2vs4I8ftM1EqqoE2M3r6kt3AC';
1135          $jwt .= 'EscmqzizI3j80USBCLUUb1UTsfJb2g7oyApJAp-13Q3InR3QyvWO8unG5VraFE7IL5I28h';
1136          $jwt .= 'MkQAHuCI90DFmXB4leflAu7wNlIK_U8xkGl8X8Mnv6MWgg94Ki8jgIq_kA85JAqI';
1137  
1138          lti_verify_jwt_signature($typeid, '', $jwt);
1139      }
1140  
1141      /**
1142       * Test lti_verify_jwt_signature().
1143       */
1144      public function test_lti_verify_jwt_signature_with_lti2() {
1145          $this->resetAfterTest();
1146  
1147          $this->setAdminUser();
1148  
1149          // Create a tool proxy.
1150          $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array());
1151  
1152          // Create a tool type, associated with that proxy.
1153          $type = new \stdClass();
1154          $type->state = LTI_TOOL_STATE_CONFIGURED;
1155          $type->name = "Test tool";
1156          $type->description = "Example description";
1157          $type->toolproxyid = $proxy->id;
1158          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1159  
1160          $data = new \stdClass();
1161          $data->lti_contentitem = true;
1162  
1163          $typeid = lti_add_type($type, $data);
1164  
1165          $this->expectExceptionMessage('JWT security not supported with LTI 2');
1166          lti_verify_jwt_signature($typeid, '', '');
1167      }
1168  
1169      /**
1170       * Test lti_verify_jwt_signature().
1171       */
1172      public function test_lti_verify_jwt_signature_no_consumer_key() {
1173          $this->resetAfterTest();
1174  
1175          $this->setAdminUser();
1176  
1177          // Create a tool type, associated with that proxy.
1178          $type = new \stdClass();
1179          $type->state = LTI_TOOL_STATE_CONFIGURED;
1180          $type->name = "Test tool";
1181          $type->description = "Example description";
1182          $type->clientid = 'consumerkey';
1183          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1184  
1185          $config = new \stdClass();
1186          $typeid = lti_add_type($type, $config);
1187  
1188          $this->expectExceptionMessage(get_string('errorincorrectconsumerkey', 'mod_lti'));
1189          lti_verify_jwt_signature($typeid, '', '');
1190      }
1191  
1192      /**
1193       * Test lti_verify_jwt_signature().
1194       */
1195      public function test_lti_verify_jwt_signature_no_public_key() {
1196          $this->resetAfterTest();
1197          $this->setAdminUser();
1198  
1199          // Create a tool type, associated with that proxy.
1200          $type = new \stdClass();
1201          $type->state = LTI_TOOL_STATE_CONFIGURED;
1202          $type->name = "Test tool";
1203          $type->description = "Example description";
1204          $type->clientid = 'consumerkey';
1205          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1206  
1207          $config = new \stdClass();
1208          $config->lti_keytype = LTI_RSA_KEY;
1209          $typeid = lti_add_type($type, $config);
1210  
1211          $this->expectExceptionMessage('No public key configured');
1212          lti_verify_jwt_signature($typeid, 'consumerkey', '');
1213      }
1214  
1215      /**
1216       * Test lti_convert_content_items().
1217       */
1218      public function test_lti_convert_content_items() {
1219          $contentitems = [];
1220          $contentitems[] = [
1221              'type' => 'ltiResourceLink',
1222              'url' => 'http://example.com/messages/launch',
1223              'title' => 'Test title',
1224              'text' => 'Test text',
1225              'iframe' => []
1226          ];
1227          $contentitems[] = [
1228              'type' => 'ltiResourceLink',
1229              'url' => 'http://example.com/messages/launch2',
1230              'title' => 'Test title2',
1231              'text' => 'Test text2',
1232              'iframe' => [
1233                  'height' => 200,
1234                  'width' => 300
1235              ],
1236              'window' => []
1237          ];
1238          $contentitems[] = [
1239              'type' => 'ltiResourceLink',
1240              'url' => 'http://example.com/messages/launch3',
1241              'title' => 'Test title3',
1242              'text' => 'Test text3',
1243              'window' => [
1244                  'targetName' => 'test-win',
1245                  'height' => 400
1246              ]
1247          ];
1248  
1249          $contentitems = json_encode($contentitems);
1250  
1251          $json = lti_convert_content_items($contentitems);
1252  
1253          $jsondecode = json_decode($json);
1254  
1255          $strcontext = '@context';
1256          $strgraph = '@graph';
1257          $strtype = '@type';
1258  
1259          $objgraph = new \stdClass();
1260          $objgraph->url = 'http://example.com/messages/launch';
1261          $objgraph->title = 'Test title';
1262          $objgraph->text = 'Test text';
1263          $objgraph->placementAdvice = new \stdClass();
1264          $objgraph->placementAdvice->presentationDocumentTarget = 'iframe';
1265          $objgraph->{$strtype} = 'LtiLinkItem';
1266          $objgraph->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1267  
1268          $objgraph2 = new \stdClass();
1269          $objgraph2->url = 'http://example.com/messages/launch2';
1270          $objgraph2->title = 'Test title2';
1271          $objgraph2->text = 'Test text2';
1272          $objgraph2->placementAdvice = new \stdClass();
1273          $objgraph2->placementAdvice->presentationDocumentTarget = 'iframe';
1274          $objgraph2->placementAdvice->displayHeight = 200;
1275          $objgraph2->placementAdvice->displayWidth = 300;
1276          $objgraph2->{$strtype} = 'LtiLinkItem';
1277          $objgraph2->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1278  
1279          $objgraph3 = new \stdClass();
1280          $objgraph3->url = 'http://example.com/messages/launch3';
1281          $objgraph3->title = 'Test title3';
1282          $objgraph3->text = 'Test text3';
1283          $objgraph3->placementAdvice = new \stdClass();
1284          $objgraph3->placementAdvice->presentationDocumentTarget = 'window';
1285          $objgraph3->placementAdvice->displayHeight = 400;
1286          $objgraph3->placementAdvice->windowTarget = 'test-win';
1287          $objgraph3->{$strtype} = 'LtiLinkItem';
1288          $objgraph3->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1289  
1290          $expected = new \stdClass();
1291          $expected->{$strcontext} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1292          $expected->{$strgraph} = [];
1293          $expected->{$strgraph}[] = $objgraph;
1294          $expected->{$strgraph}[] = $objgraph2;
1295          $expected->{$strgraph}[] = $objgraph3;
1296  
1297          $this->assertEquals($expected, $jsondecode);
1298      }
1299  
1300      /**
1301       * Test adding a single gradable item through content item.
1302       */
1303      public function test_lti_tool_configuration_from_content_item_single_gradable() {
1304          $this->resetAfterTest();
1305          $this->setAdminUser();
1306  
1307          $type = new \stdClass();
1308          $type->name = "Test tool";
1309          $type->baseurl = "http://example.com";
1310          $config = new \stdClass();
1311          $config->lti_acceptgrades = LTI_SETTING_DELEGATE;
1312          $typeid = lti_add_type($type, $config);
1313  
1314          $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
1315          $contentitems = [];
1316          $contentitems[] = [
1317              'type' => 'ltiResourceLink',
1318              'url' => 'http://example.com/messages/launch',
1319              'title' => 'Test title',
1320              'lineItem' => [
1321                  'resourceId' => 'r12345',
1322                  'tag' => 'final',
1323                  'scoreMaximum' => 10.0
1324              ],
1325              'frame' => []
1326          ];
1327          $contentitemsjson13 = json_encode($contentitems);
1328          $json11 = lti_convert_content_items($contentitemsjson13);
1329  
1330          $config = lti_tool_configuration_from_content_item($typeid,
1331                                                             'ContentItemSelection',
1332                                                             $type->ltiversion,
1333                                                             'ConsumerKey',
1334                                                             $json11);
1335  
1336          $this->assertEquals($contentitems[0]['url'], $config->toolurl);
1337          $this->assertEquals(LTI_SETTING_ALWAYS, $config->instructorchoiceacceptgrades);
1338          $this->assertEquals($contentitems[0]['lineItem']['tag'], $config->lineitemtag);
1339          $this->assertEquals($contentitems[0]['lineItem']['resourceId'], $config->lineitemresourceid);
1340          $this->assertEquals($contentitems[0]['lineItem']['scoreMaximum'], $config->grade_modgrade_point);
1341      }
1342  
1343      /**
1344       * Test adding multiple gradable items through content item.
1345       */
1346      public function test_lti_tool_configuration_from_content_item_multiple() {
1347          $this->resetAfterTest();
1348          $this->setAdminUser();
1349  
1350          $type = new \stdClass();
1351          $type->name = "Test tool";
1352          $type->baseurl = "http://example.com";
1353          $config = new \stdClass();
1354          $config->lti_acceptgrades = LTI_SETTING_DELEGATE;
1355          $typeid = lti_add_type($type, $config);
1356  
1357          $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
1358          $contentitems = [];
1359          $contentitems[] = [
1360              'type' => 'ltiResourceLink',
1361              'url' => 'http://example.com/messages/launch',
1362              'title' => 'Test title',
1363              'text' => 'Test text',
1364              'icon' => [
1365                  'url' => 'http://lti.example.com/image.jpg',
1366                  'width' => 100
1367              ],
1368              'frame' => []
1369          ];
1370          $contentitems[] = [
1371              'type' => 'ltiResourceLink',
1372              'url' => 'http://example.com/messages/launchgraded',
1373              'title' => 'Test Graded',
1374              'lineItem' => [
1375                  'resourceId' => 'r12345',
1376                  'tag' => 'final',
1377                  'scoreMaximum' => 10.0
1378              ],
1379              'frame' => []
1380          ];
1381          $contentitemsjson13 = json_encode($contentitems);
1382          $json11 = lti_convert_content_items($contentitemsjson13);
1383  
1384          $config = lti_tool_configuration_from_content_item($typeid,
1385                                                             'ContentItemSelection',
1386                                                             $type->ltiversion,
1387                                                             'ConsumerKey',
1388                                                             $json11);
1389          $this->assertNotNull($config->multiple);
1390          $this->assertEquals(2, count( $config->multiple ));
1391          $this->assertEquals($contentitems[0]['title'], $config->multiple[0]->name);
1392          $this->assertEquals($contentitems[0]['url'], $config->multiple[0]->toolurl);
1393          $this->assertEquals(LTI_SETTING_NEVER, $config->multiple[0]->instructorchoiceacceptgrades);
1394          $this->assertEquals($contentitems[1]['url'], $config->multiple[1]->toolurl);
1395          $this->assertEquals(LTI_SETTING_ALWAYS, $config->multiple[1]->instructorchoiceacceptgrades);
1396          $this->assertEquals($contentitems[1]['lineItem']['tag'], $config->multiple[1]->lineitemtag);
1397          $this->assertEquals($contentitems[1]['lineItem']['resourceId'], $config->multiple[1]->lineitemresourceid);
1398          $this->assertEquals($contentitems[1]['lineItem']['scoreMaximum'], $config->multiple[1]->grade_modgrade_point);
1399      }
1400  
1401      /**
1402       * Test adding a single non gradable item through content item.
1403       */
1404      public function test_lti_tool_configuration_from_content_item_single() {
1405          $this->resetAfterTest();
1406          $this->setAdminUser();
1407  
1408          $type = new \stdClass();
1409          $type->name = "Test tool";
1410          $type->baseurl = "http://example.com";
1411          $config = new \stdClass();
1412          $typeid = lti_add_type($type, $config);
1413  
1414          $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
1415          $contentitems = [];
1416          $contentitems[] = [
1417              'type' => 'ltiResourceLink',
1418              'url' => 'http://example.com/messages/launch',
1419              'title' => 'Test title',
1420              'text' => 'Test text',
1421              'icon' => [
1422                  'url' => 'http://lti.example.com/image.jpg',
1423                  'width' => 100
1424              ],
1425              'frame' => []
1426          ];
1427          $contentitemsjson13 = json_encode($contentitems);
1428          $json11 = lti_convert_content_items($contentitemsjson13);
1429  
1430          $config = lti_tool_configuration_from_content_item($typeid,
1431                                                             'ContentItemSelection',
1432                                                             $type->ltiversion,
1433                                                             'ConsumerKey',
1434                                                             $json11);
1435          $this->assertEquals($contentitems[0]['title'], $config->name);
1436          $this->assertEquals($contentitems[0]['text'], $config->introeditor['text']);
1437          $this->assertEquals($contentitems[0]['url'], $config->toolurl);
1438          $this->assertEquals($contentitems[0]['icon']['url'], $config->icon);
1439          $this->assertEquals(LTI_SETTING_NEVER, $config->instructorchoiceacceptgrades);
1440  
1441      }
1442  
1443      /**
1444       * Test lti_sign_jwt().
1445       */
1446      public function test_lti_sign_jwt() {
1447          $this->resetAfterTest();
1448  
1449          $this->setAdminUser();
1450  
1451          // Create a tool type, associated with that proxy.
1452          $type = new \stdClass();
1453          $type->state = LTI_TOOL_STATE_CONFIGURED;
1454          $type->name = "Test tool";
1455          $type->description = "Example description";
1456          $type->clientid = 'consumerkey';
1457          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1458  
1459          $config = new \stdClass();
1460          $typeid = lti_add_type($type, $config);
1461  
1462          $params = [];
1463          $params['roles'] = 'urn:lti:role:ims/lis/testrole,' .
1464              'urn:lti:instrole:ims/lis/testinstrole,' .
1465              'urn:lti:sysrole:ims/lis/testsysrole,' .
1466              'hi';
1467          $params['accept_copy_advice'] = [
1468              'suffix' => 'dl',
1469              'group' => 'deep_linking_settings',
1470              'claim' => 'accept_copy_advice',
1471              'isarray' => false
1472          ];
1473          $params['lis_result_sourcedid'] = [
1474              'suffix' => 'bos',
1475              'group' => 'basicoutcomesservice',
1476              'claim' => 'lis_result_sourcedid',
1477              'isarray' => false
1478          ];
1479          $endpoint = 'https://www.example.com/moodle';
1480          $oauthconsumerkey = 'consumerkey';
1481          $nonce = '';
1482  
1483          $jwt = lti_sign_jwt($params, $endpoint, $oauthconsumerkey, $typeid, $nonce);
1484  
1485          $this->assertArrayHasKey('id_token', $jwt);
1486          $this->assertNotEmpty($jwt['id_token']);
1487      }
1488  
1489      /**
1490       * Test lti_convert_from_jwt()
1491       */
1492      public function test_lti_convert_from_jwt() {
1493          $this->resetAfterTest();
1494  
1495          $this->setAdminUser();
1496  
1497          // Create a tool type, associated with that proxy.
1498          $type = new \stdClass();
1499          $type->state = LTI_TOOL_STATE_CONFIGURED;
1500          $type->name = "Test tool";
1501          $type->description = "Example description";
1502          $type->clientid = 'sso.example.com';
1503          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1504  
1505          $config = new \stdClass();
1506          $config->lti_publickey = '-----BEGIN PUBLIC KEY-----
1507  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
1508  vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
1509  aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
1510  tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
1511  e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
1512  V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
1513  MwIDAQAB
1514  -----END PUBLIC KEY-----';
1515          $config->lti_keytype = LTI_RSA_KEY;
1516  
1517          $typeid = lti_add_type($type, $config);
1518  
1519          $params = lti_convert_from_jwt($typeid, 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwib' .
1520              'mFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoic3NvLmV4YW1wbGUuY29tIn0.XURVvEb5ueAvFsn-S9EB' .
1521              'BSfKbsgUzfRQqmJ6evlrYdx7sXWoZXw1nYjaLTg-mawvBr7MVvrdG9qh6oN8OfkQ7bfMwiz4tjBMJ4B4q_sig5BDYIKwMNjZL5GGCBs89FQrgqZBhxw' .
1522              '3exTjPBEn69__w40o0AhCsBohPMh0ZsAyHug5dhm8vIuOP667repUJzM8uKCD6L4bEL6vQE8EwU6WQOmfJ2SDmRs-1pFkiaFd6hmPn6AVX7ETtzQmlT' .
1523              'X-nXe9weQjU1lH4AQG2Yfnn-7lS94bt6E76Zt-XndP3IY7W48EpnRfUK9Ff1fZlomT4MPahdNP1eP8gT2iMz7vYpCfmA');
1524  
1525          $this->assertEquals('sso.example.com', $params['oauth_consumer_key']);
1526          $this->assertEquals('John Doe', $params['lis_person_name_full']);
1527      }
1528  
1529      /**
1530       * Test lti_get_permitted_service_scopes().
1531       */
1532      public function test_lti_get_permitted_service_scopes() {
1533          $this->resetAfterTest();
1534  
1535          $this->setAdminUser();
1536  
1537          // Create a tool type, associated with that proxy.
1538          $type = new \stdClass();
1539          $type->state = LTI_TOOL_STATE_CONFIGURED;
1540          $type->name = "Test tool";
1541          $type->description = "Example description";
1542          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1543  
1544          $typeconfig = new \stdClass();
1545          $typeconfig->lti_acceptgrades = true;
1546  
1547          $typeid = lti_add_type($type, $typeconfig);
1548  
1549          $tool = lti_get_type($typeid);
1550  
1551          $config = lti_get_type_config($typeid);
1552          $permittedscopes = lti_get_permitted_service_scopes($tool, $config);
1553  
1554          $expected = [
1555              'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome'
1556          ];
1557          $this->assertEquals($expected, $permittedscopes);
1558      }
1559  
1560      /**
1561       * Test get_tool_type_config().
1562       */
1563      public function test_get_tool_type_config() {
1564          $this->resetAfterTest();
1565  
1566          $this->setAdminUser();
1567  
1568          // Create a tool type, associated with that proxy.
1569          $type = new \stdClass();
1570          $type->state = LTI_TOOL_STATE_CONFIGURED;
1571          $type->name = "Test tool";
1572          $type->description = "Example description";
1573          $type->clientid = "Test client ID";
1574          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1575  
1576          $config = new \stdClass();
1577  
1578          $typeid = lti_add_type($type, $config);
1579  
1580          $type = lti_get_type($typeid);
1581  
1582          $typeconfig = get_tool_type_config($type);
1583  
1584          $this->assertEquals('https://www.example.com/moodle', $typeconfig['platformid']);
1585          $this->assertEquals($type->clientid, $typeconfig['clientid']);
1586          $this->assertEquals($typeid, $typeconfig['deploymentid']);
1587          $this->assertEquals('https://www.example.com/moodle/mod/lti/certs.php', $typeconfig['publickeyseturl']);
1588          $this->assertEquals('https://www.example.com/moodle/mod/lti/token.php', $typeconfig['accesstokenurl']);
1589          $this->assertEquals('https://www.example.com/moodle/mod/lti/auth.php', $typeconfig['authrequesturl']);
1590      }
1591  
1592      /**
1593       * Test lti_new_access_token().
1594       */
1595      public function test_lti_new_access_token() {
1596          global $DB;
1597  
1598          $this->resetAfterTest();
1599  
1600          $this->setAdminUser();
1601  
1602          // Create a tool type, associated with that proxy.
1603          $type = new \stdClass();
1604          $type->state = LTI_TOOL_STATE_CONFIGURED;
1605          $type->name = "Test tool";
1606          $type->description = "Example description";
1607          $type->clientid = "Test client ID";
1608          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1609  
1610          $config = new \stdClass();
1611  
1612          $typeid = lti_add_type($type, $config);
1613  
1614          $scopes = ['lti_some_scope', 'lti_another_scope'];
1615  
1616          lti_new_access_token($typeid, $scopes);
1617  
1618          $token = $DB->get_records('lti_access_tokens');
1619          $this->assertEquals(1, count($token));
1620  
1621          $token = reset($token);
1622  
1623          $this->assertEquals($typeid, $token->typeid);
1624          $this->assertEquals(json_encode(array_values($scopes)), $token->scope);
1625          $this->assertEquals($token->timecreated + LTI_ACCESS_TOKEN_LIFE, $token->validuntil);
1626          $this->assertNull($token->lastaccess);
1627      }
1628  
1629      /**
1630       * Test lti_build_login_request().
1631       */
1632      public function test_lti_build_login_request() {
1633          global $USER, $CFG;
1634  
1635          $this->resetAfterTest();
1636  
1637          $USER->id = 123456789;
1638  
1639          $course   = $this->getDataGenerator()->create_course();
1640          $instance = $this->getDataGenerator()->create_module('lti',
1641              [
1642                  'course' => $course->id,
1643              ]
1644          );
1645  
1646          $config = new \stdClass();
1647          $config->lti_clientid = 'some-client-id';
1648          $config->typeid = 'some-type-id';
1649          $config->lti_toolurl = 'some-lti-tool-url';
1650  
1651          $request = lti_build_login_request($course->id, $instance->id, $instance, $config, 'basic-lti-launch-request');
1652  
1653          $this->assertEquals($CFG->wwwroot, $request['iss']);
1654          $this->assertEquals('http://some-lti-tool-url', $request['target_link_uri']);
1655          $this->assertEquals(123456789, $request['login_hint']);
1656          $this->assertEquals($instance->id, $request['lti_message_hint']);
1657          $this->assertEquals('some-client-id', $request['client_id']);
1658          $this->assertEquals('some-type-id', $request['lti_deployment_id']);
1659      }
1660  
1661      /**
1662       * Test default orgid is host if not specified in config (tool installed in earlier version of Moodle).
1663       */
1664      public function test_lti_get_launch_data_default_organizationid_unset_usehost() {
1665          global $DB;
1666          $this->resetAfterTest();
1667          $this->setAdminUser();
1668          $config = new \stdClass();
1669          $config->lti_organizationid = '';
1670          $course = $this->getDataGenerator()->create_course();
1671          $type = $this->create_type($config);
1672          $link = $this->create_instance($type, $course);
1673          $launchdata = lti_get_launch_data($link);
1674          $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'www.example.com');
1675      }
1676  
1677      /**
1678       * Test default org id is set to host when config is usehost.
1679       */
1680      public function test_lti_get_launch_data_default_organizationid_set_usehost() {
1681          global $DB;
1682          $this->resetAfterTest();
1683          $this->setAdminUser();
1684          $config = new \stdClass();
1685          $config->lti_organizationid = '';
1686          $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
1687          $course = $this->getDataGenerator()->create_course();
1688          $type = $this->create_type($config);
1689          $link = $this->create_instance($type, $course);
1690          $launchdata = lti_get_launch_data($link);
1691          $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'www.example.com');
1692      }
1693  
1694      /**
1695       * Test default org id is set to site id when config is usesiteid.
1696       */
1697      public function test_lti_get_launch_data_default_organizationid_set_usesiteid() {
1698          global $DB;
1699          $this->resetAfterTest();
1700          $this->setAdminUser();
1701          $config = new \stdClass();
1702          $config->lti_organizationid = '';
1703          $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID;
1704          $course = $this->getDataGenerator()->create_course();
1705          $type = $this->create_type($config);
1706          $link = $this->create_instance($type, $course);
1707          $launchdata = lti_get_launch_data($link);
1708          $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], md5(get_site_identifier()));
1709      }
1710  
1711      /**
1712       * Test orgid can be overridden in which case default is ignored.
1713       */
1714      public function test_lti_get_launch_data_default_organizationid_orgid_override() {
1715          global $DB;
1716          $this->resetAfterTest();
1717          $this->setAdminUser();
1718          $config = new \stdClass();
1719          $config->lti_organizationid = 'overridden!';
1720          $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID;
1721          $course = $this->getDataGenerator()->create_course();
1722          $type = $this->create_type($config);
1723          $link = $this->create_instance($type, $course);
1724          $launchdata = lti_get_launch_data($link);
1725          $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'overridden!');
1726      }
1727  
1728      public function test_get_course_history() {
1729          global $DB;
1730          $this->resetAfterTest();
1731          $this->setAdminUser();
1732          $parentparentcourse = $this->getDataGenerator()->create_course();
1733          $parentcourse = $this->getDataGenerator()->create_course();
1734          $parentcourse->originalcourseid = $parentparentcourse->id;
1735          $DB->update_record('course', $parentcourse);
1736          $course = $this->getDataGenerator()->create_course();
1737          $course->originalcourseid = $parentcourse->id;
1738          $DB->update_record('course', $course);
1739          $this->assertEquals(get_course_history($parentparentcourse), []);
1740          $this->assertEquals(get_course_history($parentcourse), [$parentparentcourse->id]);
1741          $this->assertEquals(get_course_history($course), [$parentcourse->id, $parentparentcourse->id]);
1742          $course->originalcourseid = 38903;
1743          $DB->update_record('course', $course);
1744          $this->assertEquals(get_course_history($course), [38903]);
1745      }
1746  
1747      /**
1748       * Test the lti_get_ims_role helper function.
1749       *
1750       * @dataProvider lti_get_ims_role_provider
1751       * @covers ::lti_get_ims_role()
1752       *
1753       * @param bool $islti2 whether the method is called with LTI 2.0 role names or not.
1754       * @param string $rolename the name of the role (student, teacher, admin)
1755       * @param null|string $switchedto the role to switch to, or false if not using the 'switch to' functionality.
1756       * @param string $expected the expected role name.
1757       */
1758      public function test_lti_get_ims_role(bool $islti2, string $rolename, ?string $switchedto, string $expected) {
1759          global $DB;
1760          $this->resetAfterTest();
1761  
1762          $course = $this->getDataGenerator()->create_course();
1763          $user = $rolename == 'admin' ? get_admin() : $this->getDataGenerator()->create_and_enrol($course, $rolename);
1764  
1765          if ($switchedto) {
1766              $this->setUser($user);
1767              $role = $DB->get_record('role', array('shortname' => $switchedto));
1768              role_switch($role->id, \context_course::instance($course->id));
1769          }
1770  
1771          $this->assertEquals($expected, lti_get_ims_role($user, 0, $course->id, $islti2));
1772      }
1773  
1774      /**
1775       * Data provider for testing lti_get_ims_role.
1776       *
1777       * @return array[] the test case data.
1778       */
1779      public function lti_get_ims_role_provider() {
1780          return [
1781              'Student, LTI 1.1, no role switch' => [
1782                  'islti2' => false,
1783                  'rolename' => 'student',
1784                  'switchedto' => null,
1785                  'expected' => 'Learner'
1786              ],
1787              'Student, LTI 2.0, no role switch' => [
1788                  'islti2' => true,
1789                  'rolename' => 'student',
1790                  'switchedto' => null,
1791                  'expected' => 'Learner'
1792              ],
1793              'Teacher, LTI 1.1, no role switch' => [
1794                  'islti2' => false,
1795                  'rolename' => 'editingteacher',
1796                  'switchedto' => null,
1797                  'expected' => 'Instructor'
1798              ],
1799              'Teacher, LTI 2.0, no role switch' => [
1800                  'islti2' => true,
1801                  'rolename' => 'editingteacher',
1802                  'switchedto' => null,
1803                  'expected' => 'Instructor'
1804              ],
1805              'Admin, LTI 1.1, no role switch' => [
1806                  'islti2' => false,
1807                  'rolename' => 'admin',
1808                  'switchedto' => null,
1809                  'expected' => 'Instructor,urn:lti:sysrole:ims/lis/Administrator,urn:lti:instrole:ims/lis/Administrator'
1810              ],
1811              'Admin, LTI 2.0, no role switch' => [
1812                  'islti2' => true,
1813                  'rolename' => 'admin',
1814                  'switchedto' => null,
1815                  'expected' => 'Instructor,http://purl.imsglobal.org/vocab/lis/v2/person#Administrator'
1816              ],
1817              'Admin, LTI 1.1, role switch student' => [
1818                  'islti2' => false,
1819                  'rolename' => 'admin',
1820                  'switchedto' => 'student',
1821                  'expected' => 'Learner'
1822              ],
1823              'Admin, LTI 2.0, role switch student' => [
1824                  'islti2' => true,
1825                  'rolename' => 'admin',
1826                  'switchedto' => 'student',
1827                  'expected' => 'Learner'
1828              ],
1829              'Admin, LTI 1.1, role switch teacher' => [
1830                  'islti2' => false,
1831                  'rolename' => 'admin',
1832                  'switchedto' => 'editingteacher',
1833                  'expected' => 'Instructor'
1834              ],
1835              'Admin, LTI 2.0, role switch teacher' => [
1836                  'islti2' => true,
1837                  'rolename' => 'admin',
1838                  'switchedto' => 'editingteacher',
1839                  'expected' => 'Instructor'
1840              ],
1841          ];
1842      }
1843  
1844      /**
1845       * Verify that empty curl responses lead to the proper moodle_exception, not to XML ValueError.
1846       *
1847       * @covers ::lti_load_cartridge()
1848       */
1849      public function test_empty_reponse_lti_load_cartridge() {
1850          // Mock the curl response to empty string, this is hardly
1851          // reproducible in real life (only Windows + GHA).
1852          \curl::mock_response('');
1853  
1854          $this->expectException(\moodle_exception::class);
1855          lti_load_cartridge('http://example.com/mocked/empty/response', []);
1856      }
1857  
1858      /**
1859       * Create an LTI Tool.
1860       *
1861       * @param object $config tool config.
1862       *
1863       * @return object tool.
1864       */
1865      private function create_type(object $config) {
1866          $type = new \stdClass();
1867          $type->state = LTI_TOOL_STATE_CONFIGURED;
1868          $type->name = "Test tool";
1869          $type->description = "Example description";
1870          $type->clientid = "Test client ID";
1871          $type->baseurl = $this->getExternalTestFileUrl('/test.html');
1872  
1873          $configbase = new \stdClass();
1874          $configbase->lti_acceptgrades = LTI_SETTING_NEVER;
1875          $configbase->lti_sendname = LTI_SETTING_NEVER;
1876          $configbase->lti_sendemailaddr = LTI_SETTING_NEVER;
1877          $mergedconfig = (object) array_merge( (array) $configbase, (array) $config);
1878          $typeid = lti_add_type($type, $mergedconfig);
1879          return lti_get_type($typeid);
1880      }
1881  
1882      /**
1883       * Create an LTI Instance for the tool in a given course.
1884       *
1885       * @param object $type tool for which an instance should be added.
1886       * @param object $course course where the instance should be added.
1887       *
1888       * @return object instance.
1889       */
1890      private function create_instance(object $type, object $course) {
1891          $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
1892          return $generator->create_instance(array('course' => $course->id,
1893                    'toolurl' => $type->baseurl,
1894                    'typeid' => $type->id
1895                    ), array());
1896      }
1897  }