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