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