See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 // 17 // This file is part of BasicLTI4Moodle 18 // 19 // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) 20 // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web 21 // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI 22 // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS 23 // are already supporting or going to support BasicLTI. This project Implements the consumer 24 // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. 25 // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem 26 // at the GESSI research group at UPC. 27 // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI 28 // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a 29 // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. 30 // 31 // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis 32 // of the Universitat Politecnica de Catalunya http://www.upc.edu 33 // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. 34 35 /** 36 * This file contains 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 $config = new stdClass(); 1427 $config->name = ''; 1428 if (isset($item->title)) { 1429 $config->name = $item->title; 1430 } 1431 if (empty($config->name)) { 1432 $config->name = $tool->name; 1433 } 1434 if (isset($item->text)) { 1435 $config->introeditor = [ 1436 'text' => $item->text, 1437 'format' => FORMAT_PLAIN 1438 ]; 1439 } else { 1440 $config->introeditor = [ 1441 'text' => '', 1442 'format' => FORMAT_PLAIN 1443 ]; 1444 } 1445 if (isset($item->icon->{'@id'})) { 1446 $iconurl = new moodle_url($item->icon->{'@id'}); 1447 // Assign item's icon URL to secureicon or icon depending on its scheme. 1448 if (strtolower($iconurl->get_scheme()) === 'https') { 1449 $config->secureicon = $iconurl->out(false); 1450 } else { 1451 $config->icon = $iconurl->out(false); 1452 } 1453 } 1454 if (isset($item->url)) { 1455 $url = new moodle_url($item->url); 1456 $config->toolurl = $url->out(false); 1457 $config->typeid = 0; 1458 } else { 1459 $config->typeid = $tool->id; 1460 } 1461 $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER; 1462 $islti2 = $tool->ltiversion === LTI_VERSION_2; 1463 if (!$islti2 && isset($typeconfig->lti_acceptgrades)) { 1464 $acceptgrades = $typeconfig->lti_acceptgrades; 1465 if ($acceptgrades == LTI_SETTING_ALWAYS) { 1466 // We create a line item regardless if the definition contains one or not. 1467 $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS; 1468 $config->grade_modgrade_point = 100; 1469 } 1470 if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) { 1471 if (isset($item->lineItem)) { 1472 $lineitem = $item->lineItem; 1473 $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS; 1474 $maxscore = 100; 1475 if (isset($lineitem->scoreConstraints)) { 1476 $sc = $lineitem->scoreConstraints; 1477 if (isset($sc->totalMaximum)) { 1478 $maxscore = $sc->totalMaximum; 1479 } else if (isset($sc->normalMaximum)) { 1480 $maxscore = $sc->normalMaximum; 1481 } 1482 } 1483 $config->grade_modgrade_point = $maxscore; 1484 $config->lineitemresourceid = ''; 1485 $config->lineitemtag = ''; 1486 $config->lineitemsubreviewurl = ''; 1487 $config->lineitemsubreviewparams = ''; 1488 if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) { 1489 $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:''; 1490 } 1491 if (isset($lineitem->tag)) { 1492 $config->lineitemtag = $lineitem->tag?:''; 1493 } 1494 if (isset($lineitem->submissionReview)) { 1495 $subreview = $lineitem->submissionReview; 1496 $config->lineitemsubreviewurl = 'DEFAULT'; 1497 if (!empty($subreview->url)) { 1498 $config->lineitemsubreviewurl = $subreview->url; 1499 } 1500 if (isset($subreview->custom)) { 1501 $config->lineitemsubreviewparams = params_to_string($subreview->custom); 1502 } 1503 } 1504 } 1505 } 1506 } 1507 $config->instructorchoicesendname = LTI_SETTING_NEVER; 1508 $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER; 1509 $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; 1510 if (isset($item->placementAdvice->presentationDocumentTarget)) { 1511 if ($item->placementAdvice->presentationDocumentTarget === 'window') { 1512 $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW; 1513 } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') { 1514 $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; 1515 } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') { 1516 $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED; 1517 } 1518 } 1519 if (isset($item->custom)) { 1520 $config->instructorcustomparameters = params_to_string($item->custom); 1521 } 1522 return $config; 1523 } 1524 1525 /** 1526 * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the 1527 * selected content item. This configuration data can be then used when adding a tool into the course. 1528 * 1529 * @param int $typeid The tool type ID. 1530 * @param string $messagetype The value for the lti_message_type parameter. 1531 * @param string $ltiversion The value for the lti_version parameter. 1532 * @param string $consumerkey The consumer key. 1533 * @param string $contentitemsjson The JSON string for the content_items parameter. 1534 * @return stdClass The array of module information objects. 1535 * @throws moodle_exception 1536 * @throws lti\OAuthException 1537 */ 1538 function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) { 1539 $tool = lti_get_type($typeid); 1540 // Validate parameters. 1541 if (!$tool) { 1542 throw new moodle_exception('errortooltypenotfound', 'mod_lti'); 1543 } 1544 // Check lti_message_type. Show debugging if it's not set to ContentItemSelection. 1545 // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment. 1546 if ($messagetype !== 'ContentItemSelection') { 1547 debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.", 1548 DEBUG_DEVELOPER); 1549 } 1550 1551 // Check LTI versions from our side and the response's side. Show debugging if they don't match. 1552 // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment. 1553 $expectedversion = $tool->ltiversion; 1554 $islti2 = ($expectedversion === LTI_VERSION_2); 1555 if ($ltiversion !== $expectedversion) { 1556 debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," . 1557 " Response: {$ltiversion}", DEBUG_DEVELOPER); 1558 } 1559 1560 $items = json_decode($contentitemsjson); 1561 if (empty($items)) { 1562 throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson); 1563 } 1564 if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) { 1565 throw new moodle_exception('errorinvalidresponseformat', 'mod_lti'); 1566 } 1567 1568 $config = null; 1569 $items = $items->{'@graph'}; 1570 if (!empty($items)) { 1571 $typeconfig = lti_get_type_type_config($tool->id); 1572 if (count($items) == 1) { 1573 $config = content_item_to_form($tool, $typeconfig, $items[0]); 1574 } else { 1575 $multiple = []; 1576 foreach ($items as $item) { 1577 $multiple[] = content_item_to_form($tool, $typeconfig, $item); 1578 } 1579 $config = new stdClass(); 1580 $config->multiple = $multiple; 1581 } 1582 } 1583 return $config; 1584 } 1585 1586 /** 1587 * Converts the new Deep-Linking format for Content-Items to the old format. 1588 * 1589 * @param string $param JSON string representing new Deep-Linking format 1590 * @return string JSON representation of content-items 1591 */ 1592 function lti_convert_content_items($param) { 1593 $items = array(); 1594 $json = json_decode($param); 1595 if (!empty($json) && is_array($json)) { 1596 foreach ($json as $item) { 1597 if (isset($item->type)) { 1598 $newitem = clone $item; 1599 switch ($item->type) { 1600 case 'ltiResourceLink': 1601 $newitem->{'@type'} = 'LtiLinkItem'; 1602 $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink'; 1603 break; 1604 case 'link': 1605 case 'rich': 1606 $newitem->{'@type'} = 'ContentItem'; 1607 $newitem->mediaType = 'text/html'; 1608 break; 1609 case 'file': 1610 $newitem->{'@type'} = 'FileItem'; 1611 break; 1612 } 1613 unset($newitem->type); 1614 if (isset($item->html)) { 1615 $newitem->text = $item->html; 1616 unset($newitem->html); 1617 } 1618 if (isset($item->iframe)) { 1619 // DeepLinking allows multiple options to be declared as supported. 1620 // We favor iframe over new window if both are specified. 1621 $newitem->placementAdvice = new stdClass(); 1622 $newitem->placementAdvice->presentationDocumentTarget = 'iframe'; 1623 if (isset($item->iframe->width)) { 1624 $newitem->placementAdvice->displayWidth = $item->iframe->width; 1625 } 1626 if (isset($item->iframe->height)) { 1627 $newitem->placementAdvice->displayHeight = $item->iframe->height; 1628 } 1629 unset($newitem->iframe); 1630 unset($newitem->window); 1631 } else if (isset($item->window)) { 1632 $newitem->placementAdvice = new stdClass(); 1633 $newitem->placementAdvice->presentationDocumentTarget = 'window'; 1634 if (isset($item->window->targetName)) { 1635 $newitem->placementAdvice->windowTarget = $item->window->targetName; 1636 } 1637 if (isset($item->window->width)) { 1638 $newitem->placementAdvice->displayWidth = $item->window->width; 1639 } 1640 if (isset($item->window->height)) { 1641 $newitem->placementAdvice->displayHeight = $item->window->height; 1642 } 1643 unset($newitem->window); 1644 } else if (isset($item->presentation)) { 1645 // This may have been part of an early draft but is not in the final spec 1646 // so keeping it around for now in case it's actually been used. 1647 $newitem->placementAdvice = new stdClass(); 1648 if (isset($item->presentation->documentTarget)) { 1649 $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget; 1650 } 1651 if (isset($item->presentation->windowTarget)) { 1652 $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget; 1653 } 1654 if (isset($item->presentation->width)) { 1655 $newitem->placementAdvice->dislayWidth = $item->presentation->width; 1656 } 1657 if (isset($item->presentation->height)) { 1658 $newitem->placementAdvice->dislayHeight = $item->presentation->height; 1659 } 1660 unset($newitem->presentation); 1661 } 1662 if (isset($item->icon) && isset($item->icon->url)) { 1663 $newitem->icon->{'@id'} = $item->icon->url; 1664 unset($newitem->icon->url); 1665 } 1666 if (isset($item->thumbnail) && isset($item->thumbnail->url)) { 1667 $newitem->thumbnail->{'@id'} = $item->thumbnail->url; 1668 unset($newitem->thumbnail->url); 1669 } 1670 if (isset($item->lineItem)) { 1671 unset($newitem->lineItem); 1672 $newitem->lineItem = new stdClass(); 1673 $newitem->lineItem->{'@type'} = 'LineItem'; 1674 $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore'; 1675 if (isset($item->lineItem->label)) { 1676 $newitem->lineItem->label = $item->lineItem->label; 1677 } 1678 if (isset($item->lineItem->resourceId)) { 1679 $newitem->lineItem->assignedActivity = new stdClass(); 1680 $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId; 1681 } 1682 if (isset($item->lineItem->tag)) { 1683 $newitem->lineItem->tag = $item->lineItem->tag; 1684 } 1685 if (isset($item->lineItem->scoreMaximum)) { 1686 $newitem->lineItem->scoreConstraints = new stdClass(); 1687 $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits'; 1688 $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum; 1689 } 1690 if (isset($item->lineItem->submissionReview)) { 1691 $newitem->lineItem->submissionReview = $item->lineItem->submissionReview; 1692 } 1693 } 1694 $items[] = $newitem; 1695 } 1696 } 1697 } 1698 1699 $newitems = new stdClass(); 1700 $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem'; 1701 $newitems->{'@graph'} = $items; 1702 1703 return json_encode($newitems); 1704 } 1705 1706 function lti_get_tool_table($tools, $id) { 1707 global $OUTPUT; 1708 $html = ''; 1709 1710 $typename = get_string('typename', 'lti'); 1711 $baseurl = get_string('baseurl', 'lti'); 1712 $action = get_string('action', 'lti'); 1713 $createdon = get_string('createdon', 'lti'); 1714 1715 if (!empty($tools)) { 1716 $html .= " 1717 <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\"> 1718 <table id=\"{$id}_tools\"> 1719 <thead> 1720 <tr> 1721 <th>$typename</th> 1722 <th>$baseurl</th> 1723 <th>$createdon</th> 1724 <th>$action</th> 1725 </tr> 1726 </thead> 1727 "; 1728 1729 foreach ($tools as $type) { 1730 $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig')); 1731 $accept = get_string('accept', 'lti'); 1732 $update = get_string('update', 'lti'); 1733 $delete = get_string('delete', 'lti'); 1734 1735 if (empty($type->toolproxyid)) { 1736 $baseurl = new \moodle_url('/mod/lti/typessettings.php', array( 1737 'action' => 'accept', 1738 'id' => $type->id, 1739 'sesskey' => sesskey(), 1740 'tab' => $id 1741 )); 1742 $ref = $type->baseurl; 1743 } else { 1744 $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array( 1745 'action' => 'accept', 1746 'id' => $type->id, 1747 'sesskey' => sesskey(), 1748 'tab' => $id 1749 )); 1750 $ref = $type->tpname; 1751 } 1752 1753 $accepthtml = $OUTPUT->action_icon($baseurl, 1754 new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null, 1755 array('title' => $accept, 'class' => 'editing_accept')); 1756 1757 $deleteaction = 'delete'; 1758 1759 if ($type->state == LTI_TOOL_STATE_CONFIGURED) { 1760 $accepthtml = ''; 1761 } 1762 1763 if ($type->state != LTI_TOOL_STATE_REJECTED) { 1764 $deleteaction = 'reject'; 1765 $delete = get_string('reject', 'lti'); 1766 } 1767 1768 $updateurl = clone($baseurl); 1769 $updateurl->param('action', 'update'); 1770 $updatehtml = $OUTPUT->action_icon($updateurl, 1771 new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null, 1772 array('title' => $update, 'class' => 'editing_update')); 1773 1774 if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) { 1775 $deleteurl = clone($baseurl); 1776 $deleteurl->param('action', $deleteaction); 1777 $deletehtml = $OUTPUT->action_icon($deleteurl, 1778 new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null, 1779 array('title' => $delete, 'class' => 'editing_delete')); 1780 } else { 1781 $deletehtml = ''; 1782 } 1783 $html .= " 1784 <tr> 1785 <td> 1786 {$type->name} 1787 </td> 1788 <td> 1789 {$ref} 1790 </td> 1791 <td> 1792 {$date} 1793 </td> 1794 <td align=\"center\"> 1795 {$accepthtml}{$updatehtml}{$deletehtml} 1796 </td> 1797 </tr> 1798 "; 1799 } 1800 $html .= '</table></div>'; 1801 } else { 1802 $html .= get_string('no_' . $id, 'lti'); 1803 } 1804 1805 return $html; 1806 } 1807 1808 /** 1809 * This function builds the tab for a category of tool proxies 1810 * 1811 * @param object $toolproxies Tool proxy instance objects 1812 * @param string $id Category ID 1813 * 1814 * @return string HTML for tab 1815 */ 1816 function lti_get_tool_proxy_table($toolproxies, $id) { 1817 global $OUTPUT; 1818 1819 if (!empty($toolproxies)) { 1820 $typename = get_string('typename', 'lti'); 1821 $url = get_string('registrationurl', 'lti'); 1822 $action = get_string('action', 'lti'); 1823 $createdon = get_string('createdon', 'lti'); 1824 1825 $html = <<< EOD 1826 <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em"> 1827 <table id="{$id}_tool_proxies"> 1828 <thead> 1829 <tr> 1830 <th>{$typename}</th> 1831 <th>{$url}</th> 1832 <th>{$createdon}</th> 1833 <th>{$action}</th> 1834 </tr> 1835 </thead> 1836 EOD; 1837 foreach ($toolproxies as $toolproxy) { 1838 $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig')); 1839 $accept = get_string('register', 'lti'); 1840 $update = get_string('update', 'lti'); 1841 $delete = get_string('delete', 'lti'); 1842 1843 $baseurl = new \moodle_url('/mod/lti/registersettings.php', array( 1844 'action' => 'accept', 1845 'id' => $toolproxy->id, 1846 'sesskey' => sesskey(), 1847 'tab' => $id 1848 )); 1849 1850 $registerurl = new \moodle_url('/mod/lti/register.php', array( 1851 'id' => $toolproxy->id, 1852 'sesskey' => sesskey(), 1853 'tab' => 'tool_proxy' 1854 )); 1855 1856 $accepthtml = $OUTPUT->action_icon($registerurl, 1857 new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null, 1858 array('title' => $accept, 'class' => 'editing_accept')); 1859 1860 $deleteaction = 'delete'; 1861 1862 if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) { 1863 $accepthtml = ''; 1864 } 1865 1866 if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) { 1867 $delete = get_string('cancel', 'lti'); 1868 } 1869 1870 $updateurl = clone($baseurl); 1871 $updateurl->param('action', 'update'); 1872 $updatehtml = $OUTPUT->action_icon($updateurl, 1873 new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null, 1874 array('title' => $update, 'class' => 'editing_update')); 1875 1876 $deleteurl = clone($baseurl); 1877 $deleteurl->param('action', $deleteaction); 1878 $deletehtml = $OUTPUT->action_icon($deleteurl, 1879 new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null, 1880 array('title' => $delete, 'class' => 'editing_delete')); 1881 $html .= <<< EOD 1882 <tr> 1883 <td> 1884 {$toolproxy->name} 1885 </td> 1886 <td> 1887 {$toolproxy->regurl} 1888 </td> 1889 <td> 1890 {$date} 1891 </td> 1892 <td align="center"> 1893 {$accepthtml}{$updatehtml}{$deletehtml} 1894 </td> 1895 </tr> 1896 EOD; 1897 } 1898 $html .= '</table></div>'; 1899 } else { 1900 $html = get_string('no_' . $id, 'lti'); 1901 } 1902 1903 return $html; 1904 } 1905 1906 /** 1907 * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter 1908 * 1909 * @param object $tool Tool instance object 1910 * 1911 * @return array List of enabled capabilities 1912 */ 1913 function lti_get_enabled_capabilities($tool) { 1914 if (!isset($tool)) { 1915 return array(); 1916 } 1917 if (!empty($tool->enabledcapability)) { 1918 $enabledcapabilities = explode("\n", $tool->enabledcapability); 1919 } else { 1920 $enabledcapabilities = array(); 1921 } 1922 if (!empty($tool->parameter)) { 1923 $paramstr = str_replace("\r\n", "\n", $tool->parameter); 1924 $paramstr = str_replace("\n\r", "\n", $paramstr); 1925 $paramstr = str_replace("\r", "\n", $paramstr); 1926 $params = explode("\n", $paramstr); 1927 foreach ($params as $param) { 1928 $pos = strpos($param, '='); 1929 if (($pos === false) || ($pos < 1)) { 1930 continue; 1931 } 1932 $value = trim(core_text::substr($param, $pos + 1, strlen($param))); 1933 if (substr($value, 0, 1) == '$') { 1934 $value = substr($value, 1); 1935 if (!in_array($value, $enabledcapabilities)) { 1936 $enabledcapabilities[] = $value; 1937 } 1938 } 1939 } 1940 } 1941 return $enabledcapabilities; 1942 } 1943 1944 /** 1945 * Splits the custom parameters 1946 * 1947 * @param string $customstr String containing the parameters 1948 * 1949 * @return array of custom parameters 1950 */ 1951 function lti_split_parameters($customstr) { 1952 $customstr = str_replace("\r\n", "\n", $customstr); 1953 $customstr = str_replace("\n\r", "\n", $customstr); 1954 $customstr = str_replace("\r", "\n", $customstr); 1955 $lines = explode("\n", $customstr); // Or should this split on "/[\n;]/"? 1956 $retval = array(); 1957 foreach ($lines as $line) { 1958 $pos = strpos($line, '='); 1959 if ( $pos === false || $pos < 1 ) { 1960 continue; 1961 } 1962 $key = trim(core_text::substr($line, 0, $pos)); 1963 $val = trim(core_text::substr($line, $pos + 1, strlen($line))); 1964 $retval[$key] = $val; 1965 } 1966 return $retval; 1967 } 1968 1969 /** 1970 * Splits the custom parameters field to the various parameters 1971 * 1972 * @param object $toolproxy Tool proxy instance object 1973 * @param object $tool Tool instance object 1974 * @param array $params LTI launch parameters 1975 * @param string $customstr String containing the parameters 1976 * @param boolean $islti2 True if an LTI 2 tool is being launched 1977 * 1978 * @return array of custom parameters 1979 */ 1980 function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) { 1981 $splitted = lti_split_parameters($customstr); 1982 $retval = array(); 1983 foreach ($splitted as $key => $val) { 1984 $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2); 1985 $key2 = lti_map_keyname($key); 1986 $retval['custom_'.$key2] = $val; 1987 if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) { 1988 $retval['custom_'.$key] = $val; 1989 } 1990 } 1991 return $retval; 1992 } 1993 1994 /** 1995 * Adds the custom parameters to an array 1996 * 1997 * @param object $toolproxy Tool proxy instance object 1998 * @param object $tool Tool instance object 1999 * @param array $params LTI launch parameters 2000 * @param array $parameters Array containing the parameters 2001 * 2002 * @return array Array of custom parameters 2003 */ 2004 function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) { 2005 $retval = array(); 2006 foreach ($parameters as $key => $val) { 2007 $key2 = lti_map_keyname($key); 2008 $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true); 2009 $retval['custom_'.$key2] = $val; 2010 if ($key != $key2) { 2011 $retval['custom_'.$key] = $val; 2012 } 2013 } 2014 return $retval; 2015 } 2016 2017 /** 2018 * Parse a custom parameter to replace any substitution variables 2019 * 2020 * @param object $toolproxy Tool proxy instance object 2021 * @param object $tool Tool instance object 2022 * @param array $params LTI launch parameters 2023 * @param string $value Custom parameter value 2024 * @param boolean $islti2 True if an LTI 2 tool is being launched 2025 * 2026 * @return string Parsed value of custom parameter 2027 */ 2028 function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) { 2029 // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var. 2030 global $USER, $COURSE; 2031 2032 if ($value) { 2033 if (substr($value, 0, 1) == '\\') { 2034 $value = substr($value, 1); 2035 } else if (substr($value, 0, 1) == '$') { 2036 $value1 = substr($value, 1); 2037 $enabledcapabilities = lti_get_enabled_capabilities($tool); 2038 if (!$islti2 || in_array($value1, $enabledcapabilities)) { 2039 $capabilities = lti_get_capabilities(); 2040 if (array_key_exists($value1, $capabilities)) { 2041 $val = $capabilities[$value1]; 2042 if ($val) { 2043 if (substr($val, 0, 1) != '$') { 2044 $value = $params[$val]; 2045 } else { 2046 $valarr = explode('->', substr($val, 1), 2); 2047 $value = "{${$valarr[0]}->{$valarr[1]}}"; 2048 $value = str_replace('<br />' , ' ', $value); 2049 $value = str_replace('<br>' , ' ', $value); 2050 $value = format_string($value); 2051 } 2052 } else { 2053 $value = lti_calculate_custom_parameter($value1); 2054 } 2055 } else { 2056 $val = $value; 2057 $services = lti_get_services(); 2058 foreach ($services as $service) { 2059 $service->set_tool_proxy($toolproxy); 2060 $service->set_type($tool); 2061 $value = $service->parse_value($val); 2062 if ($val != $value) { 2063 break; 2064 } 2065 } 2066 } 2067 } 2068 } 2069 } 2070 return $value; 2071 } 2072 2073 /** 2074 * Calculates the value of a custom parameter that has not been specified earlier 2075 * 2076 * @param string $value Custom parameter value 2077 * 2078 * @return string Calculated value of custom parameter 2079 */ 2080 function lti_calculate_custom_parameter($value) { 2081 global $USER, $COURSE; 2082 2083 switch ($value) { 2084 case 'Moodle.Person.userGroupIds': 2085 return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]); 2086 case 'Context.id.history': 2087 return implode(",", get_course_history($COURSE)); 2088 case 'CourseSection.timeFrame.begin': 2089 if (empty($COURSE->startdate)) { 2090 return ""; 2091 } 2092 $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC')); 2093 return $dt->format(DateTime::ATOM); 2094 case 'CourseSection.timeFrame.end': 2095 if (empty($COURSE->enddate)) { 2096 return ""; 2097 } 2098 $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC')); 2099 return $dt->format(DateTime::ATOM); 2100 } 2101 return null; 2102 } 2103 2104 /** 2105 * Build the history chain for this course using the course originalcourseid. 2106 * 2107 * @param object $course course for which the history is returned. 2108 * 2109 * @return array ids of the source course in ancestry order, immediate parent 1st. 2110 */ 2111 function get_course_history($course) { 2112 global $DB; 2113 $history = []; 2114 $parentid = $course->originalcourseid; 2115 while (!empty($parentid) && !in_array($parentid, $history)) { 2116 $history[] = $parentid; 2117 $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid)); 2118 } 2119 return $history; 2120 } 2121 2122 /** 2123 * Used for building the names of the different custom parameters 2124 * 2125 * @param string $key Parameter name 2126 * @param bool $tolower Do we want to convert the key into lower case? 2127 * @return string Processed name 2128 */ 2129 function lti_map_keyname($key, $tolower = true) { 2130 if ($tolower) { 2131 $newkey = ''; 2132 $key = core_text::strtolower(trim($key)); 2133 foreach (str_split($key) as $ch) { 2134 if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) { 2135 $newkey .= $ch; 2136 } else { 2137 $newkey .= '_'; 2138 } 2139 } 2140 } else { 2141 $newkey = $key; 2142 } 2143 return $newkey; 2144 } 2145 2146 /** 2147 * Gets the IMS role string for the specified user and LTI course module. 2148 * 2149 * @param mixed $user User object or user id 2150 * @param int $cmid The course module id of the LTI activity 2151 * @param int $courseid The course id of the LTI activity 2152 * @param boolean $islti2 True if an LTI 2 tool is being launched 2153 * 2154 * @return string A role string suitable for passing with an LTI launch 2155 */ 2156 function lti_get_ims_role($user, $cmid, $courseid, $islti2) { 2157 $roles = array(); 2158 2159 if (empty($cmid)) { 2160 // If no cmid is passed, check if the user is a teacher in the course 2161 // This allows other modules to programmatically "fake" a launch without 2162 // a real LTI instance. 2163 $context = context_course::instance($courseid); 2164 2165 if (has_capability('moodle/course:manageactivities', $context, $user)) { 2166 array_push($roles, 'Instructor'); 2167 } else { 2168 array_push($roles, 'Learner'); 2169 } 2170 } else { 2171 $context = context_module::instance($cmid); 2172 2173 if (has_capability('mod/lti:manage', $context)) { 2174 array_push($roles, 'Instructor'); 2175 } else { 2176 array_push($roles, 'Learner'); 2177 } 2178 } 2179 2180 if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) { 2181 // Make sure admins do not have the Learner role, then set admin role. 2182 $roles = array_diff($roles, array('Learner')); 2183 if (!$islti2) { 2184 array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator'); 2185 } else { 2186 array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator'); 2187 } 2188 } 2189 2190 return join(',', $roles); 2191 } 2192 2193 /** 2194 * Returns configuration details for the tool 2195 * 2196 * @param int $typeid Basic LTI tool typeid 2197 * 2198 * @return array Tool Configuration 2199 */ 2200 function lti_get_type_config($typeid) { 2201 global $DB; 2202 2203 $query = "SELECT name, value 2204 FROM {lti_types_config} 2205 WHERE typeid = :typeid1 2206 UNION ALL 2207 SELECT 'toolurl' AS name, baseurl AS value 2208 FROM {lti_types} 2209 WHERE id = :typeid2 2210 UNION ALL 2211 SELECT 'icon' AS name, icon AS value 2212 FROM {lti_types} 2213 WHERE id = :typeid3 2214 UNION ALL 2215 SELECT 'secureicon' AS name, secureicon AS value 2216 FROM {lti_types} 2217 WHERE id = :typeid4"; 2218 2219 $typeconfig = array(); 2220 $configs = $DB->get_records_sql($query, 2221 array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid)); 2222 2223 if (!empty($configs)) { 2224 foreach ($configs as $config) { 2225 $typeconfig[$config->name] = $config->value; 2226 } 2227 } 2228 2229 return $typeconfig; 2230 } 2231 2232 function lti_get_tools_by_url($url, $state, $courseid = null) { 2233 $domain = lti_get_domain_from_url($url); 2234 2235 return lti_get_tools_by_domain($domain, $state, $courseid); 2236 } 2237 2238 function lti_get_tools_by_domain($domain, $state = null, $courseid = null) { 2239 global $DB, $SITE; 2240 2241 $statefilter = ''; 2242 $coursefilter = ''; 2243 2244 if ($state) { 2245 $statefilter = 'AND state = :state'; 2246 } 2247 2248 if ($courseid && $courseid != $SITE->id) { 2249 $coursefilter = 'OR course = :courseid'; 2250 } 2251 2252 $query = "SELECT * 2253 FROM {lti_types} 2254 WHERE tooldomain = :tooldomain 2255 AND (course = :siteid $coursefilter) 2256 $statefilter"; 2257 2258 return $DB->get_records_sql($query, array( 2259 'courseid' => $courseid, 2260 'siteid' => $SITE->id, 2261 'tooldomain' => $domain, 2262 'state' => $state 2263 )); 2264 } 2265 2266 /** 2267 * Returns all basicLTI tools configured by the administrator 2268 * 2269 * @param int $course 2270 * 2271 * @return array 2272 */ 2273 function lti_filter_get_types($course) { 2274 global $DB; 2275 2276 if (!empty($course)) { 2277 $where = "WHERE t.course = :course"; 2278 $params = array('course' => $course); 2279 } else { 2280 $where = ''; 2281 $params = array(); 2282 } 2283 $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname 2284 FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id 2285 {$where}"; 2286 return $DB->get_records_sql($query, $params); 2287 } 2288 2289 /** 2290 * Given an array of tools, filter them based on their state 2291 * 2292 * @param array $tools An array of lti_types records 2293 * @param int $state One of the LTI_TOOL_STATE_* constants 2294 * @return array 2295 */ 2296 function lti_filter_tool_types(array $tools, $state) { 2297 $return = array(); 2298 foreach ($tools as $key => $tool) { 2299 if ($tool->state == $state) { 2300 $return[$key] = $tool; 2301 } 2302 } 2303 return $return; 2304 } 2305 2306 /** 2307 * Returns all lti types visible in this course 2308 * 2309 * @param int $courseid The id of the course to retieve types for 2310 * @param array $coursevisible options for 'coursevisible' field, 2311 * default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER] 2312 * @return stdClass[] All the lti types visible in the given course 2313 */ 2314 function lti_get_lti_types_by_course($courseid, $coursevisible = null) { 2315 global $DB, $SITE; 2316 2317 if ($coursevisible === null) { 2318 $coursevisible = [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]; 2319 } 2320 2321 list($coursevisiblesql, $coursevisparams) = $DB->get_in_or_equal($coursevisible, SQL_PARAMS_NAMED, 'coursevisible'); 2322 $courseconds = []; 2323 if (has_capability('mod/lti:addmanualinstance', context_course::instance($courseid))) { 2324 $courseconds[] = "course = :courseid"; 2325 } 2326 if (has_capability('mod/lti:addpreconfiguredinstance', context_course::instance($courseid))) { 2327 $courseconds[] = "course = :siteid"; 2328 } 2329 if (!$courseconds) { 2330 return []; 2331 } 2332 $coursecond = implode(" OR ", $courseconds); 2333 $query = "SELECT * 2334 FROM {lti_types} 2335 WHERE coursevisible $coursevisiblesql 2336 AND ($coursecond) 2337 AND state = :active 2338 ORDER BY name ASC"; 2339 2340 return $DB->get_records_sql($query, 2341 array('siteid' => $SITE->id, 'courseid' => $courseid, 'active' => LTI_TOOL_STATE_CONFIGURED) + $coursevisparams); 2342 } 2343 2344 /** 2345 * Returns tool types for lti add instance and edit page 2346 * 2347 * @return array Array of lti types 2348 */ 2349 function lti_get_types_for_add_instance() { 2350 global $COURSE; 2351 $admintypes = lti_get_lti_types_by_course($COURSE->id); 2352 2353 $types = array(); 2354 if (has_capability('mod/lti:addmanualinstance', context_course::instance($COURSE->id))) { 2355 $types[0] = (object)array('name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null); 2356 } 2357 2358 foreach ($admintypes as $type) { 2359 $types[$type->id] = $type; 2360 } 2361 2362 return $types; 2363 } 2364 2365 /** 2366 * Returns a list of configured types in the given course 2367 * 2368 * @param int $courseid The id of the course to retieve types for 2369 * @param int $sectionreturn section to return to for forming the URLs 2370 * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link 2371 */ 2372 function lti_get_configured_types($courseid, $sectionreturn = 0) { 2373 global $OUTPUT; 2374 $types = array(); 2375 $admintypes = lti_get_lti_types_by_course($courseid, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]); 2376 2377 foreach ($admintypes as $ltitype) { 2378 $type = new stdClass(); 2379 $type->id = $ltitype->id; 2380 $type->modclass = MOD_CLASS_ACTIVITY; 2381 $type->name = 'lti_type_' . $ltitype->id; 2382 // Clean the name. We don't want tags here. 2383 $type->title = clean_param($ltitype->name, PARAM_NOTAGS); 2384 $trimmeddescription = trim($ltitype->description ?? ''); 2385 if ($trimmeddescription != '') { 2386 // Clean the description. We don't want tags here. 2387 $type->help = clean_param($trimmeddescription, PARAM_NOTAGS); 2388 $type->helplink = get_string('modulename_shortcut_link', 'lti'); 2389 } 2390 2391 $iconurl = get_tool_type_icon_url($ltitype); 2392 $iconclass = ''; 2393 if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) { 2394 // Do not filter the icon if it is not the default LTI activity icon. 2395 $iconclass = 'nofilter'; 2396 } 2397 $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]); 2398 2399 $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'course' => $courseid, 2400 'sr' => $sectionreturn, 'typeid' => $ltitype->id)); 2401 $types[] = $type; 2402 } 2403 return $types; 2404 } 2405 2406 function lti_get_domain_from_url($url) { 2407 $matches = array(); 2408 2409 if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) { 2410 return $matches[1]; 2411 } 2412 } 2413 2414 function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) { 2415 $possibletools = lti_get_tools_by_url($url, $state, $courseid); 2416 2417 return lti_get_best_tool_by_url($url, $possibletools, $courseid); 2418 } 2419 2420 function lti_get_url_thumbprint($url) { 2421 // Parse URL requires a schema otherwise everything goes into 'path'. Fixed 5.4.7 or later. 2422 if (preg_match('/https?:\/\//', $url) !== 1) { 2423 $url = 'http://'.$url; 2424 } 2425 $urlparts = parse_url(strtolower($url)); 2426 if (!isset($urlparts['path'])) { 2427 $urlparts['path'] = ''; 2428 } 2429 2430 if (!isset($urlparts['query'])) { 2431 $urlparts['query'] = ''; 2432 } 2433 2434 if (!isset($urlparts['host'])) { 2435 $urlparts['host'] = ''; 2436 } 2437 2438 if (substr($urlparts['host'], 0, 4) === 'www.') { 2439 $urlparts['host'] = substr($urlparts['host'], 4); 2440 } 2441 2442 $urllower = $urlparts['host'] . '/' . $urlparts['path']; 2443 2444 if ($urlparts['query'] != '') { 2445 $urllower .= '?' . $urlparts['query']; 2446 } 2447 2448 return $urllower; 2449 } 2450 2451 function lti_get_best_tool_by_url($url, $tools, $courseid = null) { 2452 if (count($tools) === 0) { 2453 return null; 2454 } 2455 2456 $urllower = lti_get_url_thumbprint($url); 2457 2458 foreach ($tools as $tool) { 2459 $tool->_matchscore = 0; 2460 2461 $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl); 2462 2463 if ($urllower === $toolbaseurllower) { 2464 // 100 points for exact thumbprint match. 2465 $tool->_matchscore += 100; 2466 } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) { 2467 // 50 points if tool thumbprint starts with the base URL thumbprint. 2468 $tool->_matchscore += 50; 2469 } 2470 2471 // Prefer course tools over site tools. 2472 if (!empty($courseid)) { 2473 // Minus 10 points for not matching the course id (global tools). 2474 if ($tool->course != $courseid) { 2475 $tool->_matchscore -= 10; 2476 } 2477 } 2478 } 2479 2480 $bestmatch = array_reduce($tools, function($value, $tool) { 2481 if ($tool->_matchscore > $value->_matchscore) { 2482 return $tool; 2483 } else { 2484 return $value; 2485 } 2486 2487 }, (object)array('_matchscore' => -1)); 2488 2489 // None of the tools are suitable for this URL. 2490 if ($bestmatch->_matchscore <= 0) { 2491 return null; 2492 } 2493 2494 return $bestmatch; 2495 } 2496 2497 function lti_get_shared_secrets_by_key($key) { 2498 global $DB; 2499 2500 // Look up the shared secret for the specified key in both the types_config table (for configured tools) 2501 // And in the lti resource table for ad-hoc tools. 2502 $lti13 = LTI_VERSION_1P3; 2503 $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value 2504 FROM {lti_types_config} t1 2505 JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid 2506 JOIN {lti_types} type ON t2.typeid = type.id 2507 WHERE t1.name = 'resourcekey' 2508 AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1 2509 AND t2.name = 'password' 2510 AND type.state = :configured1 2511 AND type.ltiversion <> :ltiversion 2512 UNION 2513 SELECT tp.secret AS value 2514 FROM {lti_tool_proxies} tp 2515 JOIN {lti_types} t ON tp.id = t.toolproxyid 2516 WHERE tp.guid = :key2 2517 AND t.state = :configured2 2518 UNION 2519 SELECT password AS value 2520 FROM {lti} 2521 WHERE resourcekey = :key3"; 2522 2523 $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13, 2524 'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key)); 2525 2526 $values = array_map(function($item) { 2527 return $item->value; 2528 }, $sharedsecrets); 2529 2530 // There should really only be one shared secret per key. But, we can't prevent 2531 // more than one getting entered. For instance, if the same key is used for two tool providers. 2532 return $values; 2533 } 2534 2535 /** 2536 * Delete a Basic LTI configuration 2537 * 2538 * @param int $id Configuration id 2539 */ 2540 function lti_delete_type($id) { 2541 global $DB; 2542 2543 // We should probably just copy the launch URL to the tool instances in this case... using a single query. 2544 /* 2545 $instances = $DB->get_records('lti', array('typeid' => $id)); 2546 foreach ($instances as $instance) { 2547 $instance->typeid = 0; 2548 $DB->update_record('lti', $instance); 2549 }*/ 2550 2551 $DB->delete_records('lti_types', array('id' => $id)); 2552 $DB->delete_records('lti_types_config', array('typeid' => $id)); 2553 } 2554 2555 function lti_set_state_for_type($id, $state) { 2556 global $DB; 2557 2558 $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state)); 2559 } 2560 2561 /** 2562 * Transforms a basic LTI object to an array 2563 * 2564 * @param object $ltiobject Basic LTI object 2565 * 2566 * @return array Basic LTI configuration details 2567 */ 2568 function lti_get_config($ltiobject) { 2569 $typeconfig = (array)$ltiobject; 2570 $additionalconfig = lti_get_type_config($ltiobject->typeid); 2571 $typeconfig = array_merge($typeconfig, $additionalconfig); 2572 return $typeconfig; 2573 } 2574 2575 /** 2576 * 2577 * Generates some of the tool configuration based on the instance details 2578 * 2579 * @param int $id 2580 * 2581 * @return object configuration 2582 * 2583 */ 2584 function lti_get_type_config_from_instance($id) { 2585 global $DB; 2586 2587 $instance = $DB->get_record('lti', array('id' => $id)); 2588 $config = lti_get_config($instance); 2589 2590 $type = new \stdClass(); 2591 $type->lti_fix = $id; 2592 if (isset($config['toolurl'])) { 2593 $type->lti_toolurl = $config['toolurl']; 2594 } 2595 if (isset($config['instructorchoicesendname'])) { 2596 $type->lti_sendname = $config['instructorchoicesendname']; 2597 } 2598 if (isset($config['instructorchoicesendemailaddr'])) { 2599 $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr']; 2600 } 2601 if (isset($config['instructorchoiceacceptgrades'])) { 2602 $type->lti_acceptgrades = $config['instructorchoiceacceptgrades']; 2603 } 2604 if (isset($config['instructorchoiceallowroster'])) { 2605 $type->lti_allowroster = $config['instructorchoiceallowroster']; 2606 } 2607 2608 if (isset($config['instructorcustomparameters'])) { 2609 $type->lti_allowsetting = $config['instructorcustomparameters']; 2610 } 2611 return $type; 2612 } 2613 2614 /** 2615 * Generates some of the tool configuration based on the admin configuration details 2616 * 2617 * @param int $id 2618 * 2619 * @return stdClass Configuration details 2620 */ 2621 function lti_get_type_type_config($id) { 2622 global $DB; 2623 2624 $basicltitype = $DB->get_record('lti_types', array('id' => $id)); 2625 $config = lti_get_type_config($id); 2626 2627 $type = new \stdClass(); 2628 2629 $type->lti_typename = $basicltitype->name; 2630 2631 $type->typeid = $basicltitype->id; 2632 2633 $type->toolproxyid = $basicltitype->toolproxyid; 2634 2635 $type->lti_toolurl = $basicltitype->baseurl; 2636 2637 $type->lti_ltiversion = $basicltitype->ltiversion; 2638 2639 $type->lti_clientid = $basicltitype->clientid; 2640 $type->lti_clientid_disabled = $type->lti_clientid; 2641 2642 $type->lti_description = $basicltitype->description; 2643 2644 $type->lti_parameters = $basicltitype->parameter; 2645 2646 $type->lti_icon = $basicltitype->icon; 2647 2648 $type->lti_secureicon = $basicltitype->secureicon; 2649 2650 if (isset($config['resourcekey'])) { 2651 $type->lti_resourcekey = $config['resourcekey']; 2652 } 2653 if (isset($config['password'])) { 2654 $type->lti_password = $config['password']; 2655 } 2656 if (isset($config['publickey'])) { 2657 $type->lti_publickey = $config['publickey']; 2658 } 2659 if (isset($config['publickeyset'])) { 2660 $type->lti_publickeyset = $config['publickeyset']; 2661 } 2662 if (isset($config['keytype'])) { 2663 $type->lti_keytype = $config['keytype']; 2664 } 2665 if (isset($config['initiatelogin'])) { 2666 $type->lti_initiatelogin = $config['initiatelogin']; 2667 } 2668 if (isset($config['redirectionuris'])) { 2669 $type->lti_redirectionuris = $config['redirectionuris']; 2670 } 2671 2672 if (isset($config['sendname'])) { 2673 $type->lti_sendname = $config['sendname']; 2674 } 2675 if (isset($config['instructorchoicesendname'])) { 2676 $type->lti_instructorchoicesendname = $config['instructorchoicesendname']; 2677 } 2678 if (isset($config['sendemailaddr'])) { 2679 $type->lti_sendemailaddr = $config['sendemailaddr']; 2680 } 2681 if (isset($config['instructorchoicesendemailaddr'])) { 2682 $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr']; 2683 } 2684 if (isset($config['acceptgrades'])) { 2685 $type->lti_acceptgrades = $config['acceptgrades']; 2686 } 2687 if (isset($config['instructorchoiceacceptgrades'])) { 2688 $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades']; 2689 } 2690 if (isset($config['allowroster'])) { 2691 $type->lti_allowroster = $config['allowroster']; 2692 } 2693 if (isset($config['instructorchoiceallowroster'])) { 2694 $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster']; 2695 } 2696 2697 if (isset($config['customparameters'])) { 2698 $type->lti_customparameters = $config['customparameters']; 2699 } 2700 2701 if (isset($config['forcessl'])) { 2702 $type->lti_forcessl = $config['forcessl']; 2703 } 2704 2705 if (isset($config['organizationid_default'])) { 2706 $type->lti_organizationid_default = $config['organizationid_default']; 2707 } else { 2708 // Tool was configured before this option was available and the default then was host. 2709 $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST; 2710 } 2711 if (isset($config['organizationid'])) { 2712 $type->lti_organizationid = $config['organizationid']; 2713 } 2714 if (isset($config['organizationurl'])) { 2715 $type->lti_organizationurl = $config['organizationurl']; 2716 } 2717 if (isset($config['organizationdescr'])) { 2718 $type->lti_organizationdescr = $config['organizationdescr']; 2719 } 2720 if (isset($config['launchcontainer'])) { 2721 $type->lti_launchcontainer = $config['launchcontainer']; 2722 } 2723 2724 if (isset($config['coursevisible'])) { 2725 $type->lti_coursevisible = $config['coursevisible']; 2726 } 2727 2728 if (isset($config['contentitem'])) { 2729 $type->lti_contentitem = $config['contentitem']; 2730 } 2731 2732 if (isset($config['toolurl_ContentItemSelectionRequest'])) { 2733 $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest']; 2734 } 2735 2736 if (isset($config['debuglaunch'])) { 2737 $type->lti_debuglaunch = $config['debuglaunch']; 2738 } 2739 2740 if (isset($config['module_class_type'])) { 2741 $type->lti_module_class_type = $config['module_class_type']; 2742 } 2743 2744 // Get the parameters from the LTI services. 2745 foreach ($config as $name => $value) { 2746 if (strpos($name, 'ltiservice_') === 0) { 2747 $type->{$name} = $config[$name]; 2748 } 2749 } 2750 2751 return $type; 2752 } 2753 2754 function lti_prepare_type_for_save($type, $config) { 2755 if (isset($config->lti_toolurl)) { 2756 $type->baseurl = $config->lti_toolurl; 2757 if (isset($config->lti_tooldomain)) { 2758 $type->tooldomain = $config->lti_tooldomain; 2759 } else { 2760 $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl); 2761 } 2762 } 2763 if (isset($config->lti_description)) { 2764 $type->description = $config->lti_description; 2765 } 2766 if (isset($config->lti_typename)) { 2767 $type->name = $config->lti_typename; 2768 } 2769 if (isset($config->lti_ltiversion)) { 2770 $type->ltiversion = $config->lti_ltiversion; 2771 } 2772 if (isset($config->lti_clientid)) { 2773 $type->clientid = $config->lti_clientid; 2774 } 2775 if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) { 2776 $type->clientid = registration_helper::get()->new_clientid(); 2777 } else if (empty($type->clientid)) { 2778 $type->clientid = null; 2779 } 2780 if (isset($config->lti_coursevisible)) { 2781 $type->coursevisible = $config->lti_coursevisible; 2782 } 2783 2784 if (isset($config->lti_icon)) { 2785 $type->icon = $config->lti_icon; 2786 } 2787 if (isset($config->lti_secureicon)) { 2788 $type->secureicon = $config->lti_secureicon; 2789 } 2790 2791 $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0; 2792 $config->lti_forcessl = $type->forcessl; 2793 if (isset($config->lti_contentitem)) { 2794 $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0; 2795 $config->lti_contentitem = $type->contentitem; 2796 } 2797 if (isset($config->lti_toolurl_ContentItemSelectionRequest)) { 2798 if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) { 2799 $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest; 2800 } else { 2801 $type->toolurl_ContentItemSelectionRequest = ''; 2802 } 2803 $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest; 2804 } 2805 2806 $type->timemodified = time(); 2807 2808 unset ($config->lti_typename); 2809 unset ($config->lti_toolurl); 2810 unset ($config->lti_description); 2811 unset ($config->lti_ltiversion); 2812 unset ($config->lti_clientid); 2813 unset ($config->lti_icon); 2814 unset ($config->lti_secureicon); 2815 } 2816 2817 function lti_update_type($type, $config) { 2818 global $DB, $CFG; 2819 2820 lti_prepare_type_for_save($type, $config); 2821 2822 if (lti_request_is_using_ssl() && !empty($type->secureicon)) { 2823 $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon); 2824 } else { 2825 $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon)); 2826 } 2827 unset($config->oldicon); 2828 2829 if ($DB->update_record('lti_types', $type)) { 2830 foreach ($config as $key => $value) { 2831 if (substr($key, 0, 4) == 'lti_' && !is_null($value)) { 2832 $record = new \StdClass(); 2833 $record->typeid = $type->id; 2834 $record->name = substr($key, 4); 2835 $record->value = $value; 2836 lti_update_config($record); 2837 } 2838 if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) { 2839 $record = new \StdClass(); 2840 $record->typeid = $type->id; 2841 $record->name = $key; 2842 $record->value = $value; 2843 lti_update_config($record); 2844 } 2845 } 2846 if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) { 2847 // We need to remove the tool proxy for this tool to function under 1.3. 2848 $toolproxyid = $type->toolproxyid; 2849 $DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid)); 2850 $DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid)); 2851 $type->toolproxyid = null; 2852 $DB->update_record('lti_types', $type); 2853 } 2854 require_once($CFG->libdir.'/modinfolib.php'); 2855 if ($clearcache) { 2856 $sql = "SELECT cm.id, cm.course 2857 FROM {course_modules} cm 2858 JOIN {modules} m ON cm.module = m.id 2859 JOIN {lti} l ON l.course = cm.course 2860 WHERE m.name = :name AND l.typeid = :typeid"; 2861 2862 $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]); 2863 2864 $courseids = []; 2865 foreach ($rs as $record) { 2866 $courseids[] = $record->course; 2867 \course_modinfo::purge_course_module_cache($record->course, $record->id); 2868 } 2869 $rs->close(); 2870 $courseids = array_unique($courseids); 2871 foreach ($courseids as $courseid) { 2872 rebuild_course_cache($courseid, false, true); 2873 } 2874 } 2875 } 2876 } 2877 2878 function lti_add_type($type, $config) { 2879 global $USER, $SITE, $DB; 2880 2881 lti_prepare_type_for_save($type, $config); 2882 2883 if (!isset($type->state)) { 2884 $type->state = LTI_TOOL_STATE_PENDING; 2885 } 2886 2887 if (!isset($type->ltiversion)) { 2888 $type->ltiversion = LTI_VERSION_1; 2889 } 2890 2891 if (!isset($type->timecreated)) { 2892 $type->timecreated = time(); 2893 } 2894 2895 if (!isset($type->createdby)) { 2896 $type->createdby = $USER->id; 2897 } 2898 2899 if (!isset($type->course)) { 2900 $type->course = $SITE->id; 2901 } 2902 2903 // Create a salt value to be used for signing passed data to extension services 2904 // The outcome service uses the service salt on the instance. This can be used 2905 // for communication with services not related to a specific LTI instance. 2906 $config->lti_servicesalt = uniqid('', true); 2907 2908 $id = $DB->insert_record('lti_types', $type); 2909 2910 if ($id) { 2911 foreach ($config as $key => $value) { 2912 if (!is_null($value)) { 2913 if (substr($key, 0, 4) === 'lti_') { 2914 $fieldname = substr($key, 4); 2915 } else if (substr($key, 0, 11) !== 'ltiservice_') { 2916 continue; 2917 } else { 2918 $fieldname = $key; 2919 } 2920 2921 $record = new \StdClass(); 2922 $record->typeid = $id; 2923 $record->name = $fieldname; 2924 $record->value = $value; 2925 2926 lti_add_config($record); 2927 } 2928 } 2929 } 2930 2931 return $id; 2932 } 2933 2934 /** 2935 * Given an array of tool proxies, filter them based on their state 2936 * 2937 * @param array $toolproxies An array of lti_tool_proxies records 2938 * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants 2939 * 2940 * @return array 2941 */ 2942 function lti_filter_tool_proxy_types(array $toolproxies, $state) { 2943 $return = array(); 2944 foreach ($toolproxies as $key => $toolproxy) { 2945 if ($toolproxy->state == $state) { 2946 $return[$key] = $toolproxy; 2947 } 2948 } 2949 return $return; 2950 } 2951 2952 /** 2953 * Get the tool proxy instance given its GUID 2954 * 2955 * @param string $toolproxyguid Tool proxy GUID value 2956 * 2957 * @return object 2958 */ 2959 function lti_get_tool_proxy_from_guid($toolproxyguid) { 2960 global $DB; 2961 2962 $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid)); 2963 2964 return $toolproxy; 2965 } 2966 2967 /** 2968 * Get the tool proxy instance given its registration URL 2969 * 2970 * @param string $regurl Tool proxy registration URL 2971 * 2972 * @return array The record of the tool proxy with this url 2973 */ 2974 function lti_get_tool_proxies_from_registration_url($regurl) { 2975 global $DB; 2976 2977 return $DB->get_records_sql( 2978 'SELECT * FROM {lti_tool_proxies} 2979 WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl', 2980 array('regurl' => $regurl) 2981 ); 2982 } 2983 2984 /** 2985 * Generates some of the tool proxy configuration based on the admin configuration details 2986 * 2987 * @param int $id 2988 * 2989 * @return mixed Tool Proxy details 2990 */ 2991 function lti_get_tool_proxy($id) { 2992 global $DB; 2993 2994 $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id)); 2995 return $toolproxy; 2996 } 2997 2998 /** 2999 * Returns lti tool proxies. 3000 * 3001 * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them 3002 * @return array of basicLTI types 3003 */ 3004 function lti_get_tool_proxies($orphanedonly) { 3005 global $DB; 3006 3007 if ($orphanedonly) { 3008 $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL')); 3009 $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC'); 3010 foreach ($proxies as $key => $value) { 3011 if (in_array($value->id, $usedproxyids)) { 3012 unset($proxies[$key]); 3013 } 3014 } 3015 return $proxies; 3016 } else { 3017 return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC'); 3018 } 3019 } 3020 3021 /** 3022 * Generates some of the tool proxy configuration based on the admin configuration details 3023 * 3024 * @param int $id 3025 * 3026 * @return mixed Tool Proxy details 3027 */ 3028 function lti_get_tool_proxy_config($id) { 3029 $toolproxy = lti_get_tool_proxy($id); 3030 3031 $tp = new \stdClass(); 3032 $tp->lti_registrationname = $toolproxy->name; 3033 $tp->toolproxyid = $toolproxy->id; 3034 $tp->state = $toolproxy->state; 3035 $tp->lti_registrationurl = $toolproxy->regurl; 3036 $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered); 3037 $tp->lti_services = explode("\n", $toolproxy->serviceoffered); 3038 3039 return $tp; 3040 } 3041 3042 /** 3043 * Update the database with a tool proxy instance 3044 * 3045 * @param object $config Tool proxy definition 3046 * 3047 * @return int Record id number 3048 */ 3049 function lti_add_tool_proxy($config) { 3050 global $USER, $DB; 3051 3052 $toolproxy = new \stdClass(); 3053 if (isset($config->lti_registrationname)) { 3054 $toolproxy->name = trim($config->lti_registrationname); 3055 } 3056 if (isset($config->lti_registrationurl)) { 3057 $toolproxy->regurl = trim($config->lti_registrationurl); 3058 } 3059 if (isset($config->lti_capabilities)) { 3060 $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities); 3061 } else { 3062 $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities())); 3063 } 3064 if (isset($config->lti_services)) { 3065 $toolproxy->serviceoffered = implode("\n", $config->lti_services); 3066 } else { 3067 $func = function($s) { 3068 return $s->get_id(); 3069 }; 3070 $servicenames = array_map($func, lti_get_services()); 3071 $toolproxy->serviceoffered = implode("\n", $servicenames); 3072 } 3073 if (isset($config->toolproxyid) && !empty($config->toolproxyid)) { 3074 $toolproxy->id = $config->toolproxyid; 3075 if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) { 3076 $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; 3077 $toolproxy->guid = random_string(); 3078 $toolproxy->secret = random_string(); 3079 } 3080 $id = lti_update_tool_proxy($toolproxy); 3081 } else { 3082 $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; 3083 $toolproxy->timemodified = time(); 3084 $toolproxy->timecreated = $toolproxy->timemodified; 3085 if (!isset($toolproxy->createdby)) { 3086 $toolproxy->createdby = $USER->id; 3087 } 3088 $toolproxy->guid = random_string(); 3089 $toolproxy->secret = random_string(); 3090 $id = $DB->insert_record('lti_tool_proxies', $toolproxy); 3091 } 3092 3093 return $id; 3094 } 3095 3096 /** 3097 * Updates a tool proxy in the database 3098 * 3099 * @param object $toolproxy Tool proxy 3100 * 3101 * @return int Record id number 3102 */ 3103 function lti_update_tool_proxy($toolproxy) { 3104 global $DB; 3105 3106 $toolproxy->timemodified = time(); 3107 $id = $DB->update_record('lti_tool_proxies', $toolproxy); 3108 3109 return $id; 3110 } 3111 3112 /** 3113 * Delete a Tool Proxy 3114 * 3115 * @param int $id Tool Proxy id 3116 */ 3117 function lti_delete_tool_proxy($id) { 3118 global $DB; 3119 $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id)); 3120 $tools = $DB->get_records('lti_types', array('toolproxyid' => $id)); 3121 foreach ($tools as $tool) { 3122 lti_delete_type($tool->id); 3123 } 3124 $DB->delete_records('lti_tool_proxies', array('id' => $id)); 3125 } 3126 3127 /** 3128 * Get both LTI tool proxies and tool types. 3129 * 3130 * If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool 3131 * types. 3132 * For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types 3133 * will be returned. 3134 * 3135 * @param int $limit Maximum number of tools returned. 3136 * @param int $offset Do not return tools before offset index. 3137 * @param bool $orphanedonly If true, only return orphaned proxies. 3138 * @param int $toolproxyid If not 0, only return tool types that have this tool proxy id. 3139 * @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types. 3140 */ 3141 function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array { 3142 global $DB; 3143 3144 if ($orphanedonly) { 3145 $orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false); 3146 $countsql = helper::get_tool_proxy_sql($orphanedonly, true); 3147 $proxies = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit); 3148 $totalproxiescount = $DB->count_records_sql($countsql); 3149 } else { 3150 $proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC', 3151 '*', $offset, $limit); 3152 $totalproxiescount = $DB->count_records('lti_tool_proxies'); 3153 } 3154 3155 // Find new offset and limit for tool types after getting proxies and set up query. 3156 $typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative. 3157 $typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative. 3158 $typesparams = []; 3159 if (!empty($toolproxyid)) { 3160 $typesparams['toolproxyid'] = $toolproxyid; 3161 } 3162 3163 $types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC', 3164 '*', $typesoffset, $typeslimit); 3165 3166 return [$proxies, array_map('serialise_tool_type', $types)]; 3167 } 3168 3169 /** 3170 * Get the total number of LTI tool types and tool proxies. 3171 * 3172 * @param bool $orphanedonly If true, only count orphaned proxies. 3173 * @param int $toolproxyid If not 0, only count tool types that have this tool proxy id. 3174 * @return int Count of tools. 3175 */ 3176 function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int { 3177 global $DB; 3178 3179 $typessql = "SELECT count(*) 3180 FROM {lti_types}"; 3181 $typesparams = []; 3182 if (!empty($toolproxyid)) { 3183 $typessql .= " WHERE toolproxyid = :toolproxyid"; 3184 $typesparams['toolproxyid'] = $toolproxyid; 3185 } 3186 3187 $proxiessql = helper::get_tool_proxy_sql($orphanedonly, true); 3188 3189 $countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause(); 3190 3191 return $DB->count_records_sql($countsql, $typesparams); 3192 } 3193 3194 /** 3195 * Add a tool configuration in the database 3196 * 3197 * @param object $config Tool configuration 3198 * 3199 * @return int Record id number 3200 */ 3201 function lti_add_config($config) { 3202 global $DB; 3203 3204 return $DB->insert_record('lti_types_config', $config); 3205 } 3206 3207 /** 3208 * Updates a tool configuration in the database 3209 * 3210 * @param object $config Tool configuration 3211 * 3212 * @return mixed Record id number 3213 */ 3214 function lti_update_config($config) { 3215 global $DB; 3216 3217 $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name)); 3218 3219 if ($old) { 3220 $config->id = $old->id; 3221 $return = $DB->update_record('lti_types_config', $config); 3222 } else { 3223 $return = $DB->insert_record('lti_types_config', $config); 3224 } 3225 return $return; 3226 } 3227 3228 /** 3229 * Gets the tool settings 3230 * 3231 * @param int $toolproxyid Id of tool proxy record (or tool ID if negative) 3232 * @param int $courseid Id of course (null if system settings) 3233 * @param int $instanceid Id of course module (null if system or context settings) 3234 * 3235 * @return array Array settings 3236 */ 3237 function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) { 3238 global $DB; 3239 3240 $settings = array(); 3241 if ($toolproxyid > 0) { 3242 $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid, 3243 'course' => $courseid, 'coursemoduleid' => $instanceid)); 3244 } else { 3245 $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid, 3246 'course' => $courseid, 'coursemoduleid' => $instanceid)); 3247 } 3248 if ($settingsstr !== false) { 3249 $settings = json_decode($settingsstr, true); 3250 } 3251 return $settings; 3252 } 3253 3254 /** 3255 * Sets the tool settings ( 3256 * 3257 * @param array $settings Array of settings 3258 * @param int $toolproxyid Id of tool proxy record (or tool ID if negative) 3259 * @param int $courseid Id of course (null if system settings) 3260 * @param int $instanceid Id of course module (null if system or context settings) 3261 */ 3262 function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) { 3263 global $DB; 3264 3265 $json = json_encode($settings); 3266 if ($toolproxyid >= 0) { 3267 $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid, 3268 'course' => $courseid, 'coursemoduleid' => $instanceid)); 3269 } else { 3270 $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid, 3271 'course' => $courseid, 'coursemoduleid' => $instanceid)); 3272 } 3273 if ($record !== false) { 3274 $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time())); 3275 } else { 3276 $record = new \stdClass(); 3277 if ($toolproxyid > 0) { 3278 $record->toolproxyid = $toolproxyid; 3279 } else { 3280 $record->typeid = -$toolproxyid; 3281 } 3282 $record->course = $courseid; 3283 $record->coursemoduleid = $instanceid; 3284 $record->settings = $json; 3285 $record->timecreated = time(); 3286 $record->timemodified = $record->timecreated; 3287 $DB->insert_record('lti_tool_settings', $record); 3288 } 3289 } 3290 3291 /** 3292 * Signs the petition to launch the external tool using OAuth 3293 * 3294 * @param array $oldparms Parameters to be passed for signing 3295 * @param string $endpoint url of the external tool 3296 * @param string $method Method for sending the parameters (e.g. POST) 3297 * @param string $oauthconsumerkey 3298 * @param string $oauthconsumersecret 3299 * @return array|null 3300 */ 3301 function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) { 3302 3303 $parms = $oldparms; 3304 3305 $testtoken = ''; 3306 3307 // TODO: Switch to core oauthlib once implemented - MDL-30149. 3308 $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1(); 3309 $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null); 3310 $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms); 3311 $accreq->sign_request($hmacmethod, $testconsumer, $testtoken); 3312 3313 $newparms = $accreq->get_parameters(); 3314 3315 return $newparms; 3316 } 3317 3318 /** 3319 * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT 3320 * 3321 * @param array $parms Parameters to be passed for signing 3322 * @param string $endpoint url of the external tool 3323 * @param string $oauthconsumerkey 3324 * @param string $typeid ID of LTI tool type 3325 * @param string $nonce Nonce value to use 3326 * @return array|null 3327 */ 3328 function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') { 3329 global $CFG; 3330 3331 if (empty($typeid)) { 3332 $typeid = 0; 3333 } 3334 $messagetypemapping = lti_get_jwt_message_type_mapping(); 3335 if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) { 3336 $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']]; 3337 } 3338 if (isset($parms['roles'])) { 3339 $roles = explode(',', $parms['roles']); 3340 $newroles = array(); 3341 foreach ($roles as $role) { 3342 if (strpos($role, 'urn:lti:role:ims/lis/') === 0) { 3343 $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21); 3344 } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) { 3345 $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25); 3346 } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) { 3347 $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24); 3348 } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) { 3349 $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}"; 3350 } 3351 $newroles[] = $role; 3352 } 3353 $parms['roles'] = implode(',', $newroles); 3354 } 3355 3356 $now = time(); 3357 if (empty($nonce)) { 3358 $nonce = bin2hex(openssl_random_pseudo_bytes(10)); 3359 } 3360 $claimmapping = lti_get_jwt_claim_mapping(); 3361 $payload = array( 3362 'nonce' => $nonce, 3363 'iat' => $now, 3364 'exp' => $now + 60, 3365 ); 3366 $payload['iss'] = $CFG->wwwroot; 3367 $payload['aud'] = $oauthconsumerkey; 3368 $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid); 3369 $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint; 3370 3371 foreach ($parms as $key => $value) { 3372 $claim = LTI_JWT_CLAIM_PREFIX; 3373 if (array_key_exists($key, $claimmapping)) { 3374 $mapping = $claimmapping[$key]; 3375 $type = $mapping["type"] ?? "string"; 3376 if ($mapping['isarray']) { 3377 $value = explode(',', $value); 3378 sort($value); 3379 } else if ($type == 'boolean') { 3380 $value = isset($value) && ($value == 'true'); 3381 } 3382 if (!empty($mapping['suffix'])) { 3383 $claim .= "-{$mapping['suffix']}"; 3384 } 3385 $claim .= '/claim/'; 3386 if (is_null($mapping['group'])) { 3387 $payload[$mapping['claim']] = $value; 3388 } else if (empty($mapping['group'])) { 3389 $payload["{$claim}{$mapping['claim']}"] = $value; 3390 } else { 3391 $claim .= $mapping['group']; 3392 $payload[$claim][$mapping['claim']] = $value; 3393 } 3394 } else if (strpos($key, 'custom_') === 0) { 3395 $payload["{$claim}/claim/custom"][substr($key, 7)] = $value; 3396 } else if (strpos($key, 'ext_') === 0) { 3397 $payload["{$claim}/claim/ext"][substr($key, 4)] = $value; 3398 } 3399 } 3400 3401 $privatekey = jwks_helper::get_private_key(); 3402 $jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']); 3403 3404 $newparms = array(); 3405 $newparms['id_token'] = $jwt; 3406 3407 return $newparms; 3408 } 3409 3410 /** 3411 * Verfies the JWT and converts its claims to their equivalent message parameter. 3412 * 3413 * @param int $typeid 3414 * @param string $jwtparam JWT parameter 3415 * 3416 * @return array message parameters 3417 * @throws moodle_exception 3418 */ 3419 function lti_convert_from_jwt($typeid, $jwtparam) { 3420 3421 $params = array(); 3422 $parts = explode('.', $jwtparam); 3423 $ok = (count($parts) === 3); 3424 if ($ok) { 3425 $payload = JWT::urlsafeB64Decode($parts[1]); 3426 $claims = json_decode($payload, true); 3427 $ok = !is_null($claims) && !empty($claims['iss']); 3428 } 3429 if ($ok) { 3430 lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam); 3431 $params['oauth_consumer_key'] = $claims['iss']; 3432 foreach (lti_get_jwt_claim_mapping() as $key => $mapping) { 3433 $claim = LTI_JWT_CLAIM_PREFIX; 3434 if (!empty($mapping['suffix'])) { 3435 $claim .= "-{$mapping['suffix']}"; 3436 } 3437 $claim .= '/claim/'; 3438 if (is_null($mapping['group'])) { 3439 $claim = $mapping['claim']; 3440 } else if (empty($mapping['group'])) { 3441 $claim .= $mapping['claim']; 3442 } else { 3443 $claim .= $mapping['group']; 3444 } 3445 if (isset($claims[$claim])) { 3446 $value = null; 3447 if (empty($mapping['group'])) { 3448 $value = $claims[$claim]; 3449 } else { 3450 $group = $claims[$claim]; 3451 if (is_array($group) && array_key_exists($mapping['claim'], $group)) { 3452 $value = $group[$mapping['claim']]; 3453 } 3454 } 3455 if (!empty($value) && $mapping['isarray']) { 3456 if (is_array($value)) { 3457 if (is_array($value[0])) { 3458 $value = json_encode($value); 3459 } else { 3460 $value = implode(',', $value); 3461 } 3462 } 3463 } 3464 if (!is_null($value) && is_string($value) && (strlen($value) > 0)) { 3465 $params[$key] = $value; 3466 } 3467 } 3468 $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom'; 3469 if (isset($claims[$claim])) { 3470 $custom = $claims[$claim]; 3471 if (is_array($custom)) { 3472 foreach ($custom as $key => $value) { 3473 $params["custom_{$key}"] = $value; 3474 } 3475 } 3476 } 3477 $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext'; 3478 if (isset($claims[$claim])) { 3479 $ext = $claims[$claim]; 3480 if (is_array($ext)) { 3481 foreach ($ext as $key => $value) { 3482 $params["ext_{$key}"] = $value; 3483 } 3484 } 3485 } 3486 } 3487 } 3488 if (isset($params['content_items'])) { 3489 $params['content_items'] = lti_convert_content_items($params['content_items']); 3490 } 3491 $messagetypemapping = lti_get_jwt_message_type_mapping(); 3492 if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) { 3493 $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']]; 3494 } 3495 return $params; 3496 } 3497 3498 /** 3499 * Posts the launch petition HTML 3500 * 3501 * @param array $newparms Signed parameters 3502 * @param string $endpoint URL of the external tool 3503 * @param bool $debug Debug (true/false) 3504 * @return string 3505 */ 3506 function lti_post_launch_html($newparms, $endpoint, $debug=false) { 3507 $r = "<form action=\"" . $endpoint . 3508 "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n"; 3509 3510 // Contruct html for the launch parameters. 3511 foreach ($newparms as $key => $value) { 3512 $key = htmlspecialchars($key, ENT_COMPAT); 3513 $value = htmlspecialchars($value, ENT_COMPAT); 3514 if ( $key == "ext_submit" ) { 3515 $r .= "<input type=\"submit\""; 3516 } else { 3517 $r .= "<input type=\"hidden\" name=\"{$key}\""; 3518 } 3519 $r .= " value=\""; 3520 $r .= $value; 3521 $r .= "\"/>\n"; 3522 } 3523 3524 if ( $debug ) { 3525 $r .= "<script language=\"javascript\"> \n"; 3526 $r .= " //<![CDATA[ \n"; 3527 $r .= "function basicltiDebugToggle() {\n"; 3528 $r .= " var ele = document.getElementById(\"basicltiDebug\");\n"; 3529 $r .= " if (ele.style.display == \"block\") {\n"; 3530 $r .= " ele.style.display = \"none\";\n"; 3531 $r .= " }\n"; 3532 $r .= " else {\n"; 3533 $r .= " ele.style.display = \"block\";\n"; 3534 $r .= " }\n"; 3535 $r .= "} \n"; 3536 $r .= " //]]> \n"; 3537 $r .= "</script>\n"; 3538 $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">"; 3539 $r .= get_string("toggle_debug_data", "lti")."</a>\n"; 3540 $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n"; 3541 $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n"; 3542 $r .= $endpoint . "<br/>\n <br/>\n"; 3543 $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n"; 3544 foreach ($newparms as $key => $value) { 3545 $key = htmlspecialchars($key, ENT_COMPAT); 3546 $value = htmlspecialchars($value, ENT_COMPAT); 3547 $r .= "$key = $value<br/>\n"; 3548 } 3549 $r .= " <br/>\n"; 3550 $r .= "</div>\n"; 3551 } 3552 $r .= "</form>\n"; 3553 3554 // Auto-submit the form if endpoint is set. 3555 if ($endpoint !== '' && !$debug) { 3556 $r .= " <script type=\"text/javascript\"> \n" . 3557 " //<![CDATA[ \n" . 3558 " document.ltiLaunchForm.submit(); \n" . 3559 " //]]> \n" . 3560 " </script> \n"; 3561 } 3562 return $r; 3563 } 3564 3565 /** 3566 * Generate the form for initiating a login request for an LTI 1.3 message 3567 * 3568 * @param int $courseid Course ID 3569 * @param int $cmid LTI instance ID 3570 * @param stdClass|null $instance LTI instance 3571 * @param stdClass $config Tool type configuration 3572 * @param string $messagetype LTI message type 3573 * @param string $title Title of content item 3574 * @param string $text Description of content item 3575 * @param int $foruserid Id of the user targeted by the launch 3576 * @return string 3577 */ 3578 function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request', 3579 $title = '', $text = '', $foruserid = 0) { 3580 global $SESSION; 3581 3582 $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text); 3583 3584 $r = "<form action=\"" . $config->lti_initiatelogin . 3585 "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " . 3586 "encType=\"application/x-www-form-urlencoded\">\n"; 3587 3588 foreach ($params as $key => $value) { 3589 $key = htmlspecialchars($key, ENT_COMPAT); 3590 $value = htmlspecialchars($value, ENT_COMPAT); 3591 $r .= " <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n"; 3592 } 3593 $r .= "</form>\n"; 3594 3595 $r .= "<script type=\"text/javascript\">\n" . 3596 "//<![CDATA[\n" . 3597 "document.ltiInitiateLoginForm.submit();\n" . 3598 "//]]>\n" . 3599 "</script>\n"; 3600 3601 return $r; 3602 } 3603 3604 /** 3605 * Prepares an LTI 1.3 login request 3606 * 3607 * @param int $courseid Course ID 3608 * @param int $cmid Course Module instance ID 3609 * @param stdClass|null $instance LTI instance 3610 * @param stdClass $config Tool type configuration 3611 * @param string $messagetype LTI message type 3612 * @param int $foruserid Id of the user targeted by the launch 3613 * @param string $title Title of content item 3614 * @param string $text Description of content item 3615 * @return array Login request parameters 3616 */ 3617 function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') { 3618 global $USER, $CFG, $SESSION; 3619 $ltihint = []; 3620 if (!empty($instance)) { 3621 $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl; 3622 $launchid = 'ltilaunch'.$instance->id.'_'.rand(); 3623 $ltihint['cmid'] = $cmid; 3624 $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,"; 3625 } else { 3626 $endpoint = $config->lti_toolurl; 3627 if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) { 3628 $endpoint = $config->lti_toolurl_ContentItemSelectionRequest; 3629 } 3630 $launchid = "ltilaunch_$messagetype".rand(); 3631 $SESSION->$launchid = 3632 "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text); 3633 } 3634 $endpoint = trim($endpoint); 3635 $services = lti_get_services(); 3636 foreach ($services as $service) { 3637 [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance); 3638 } 3639 3640 $ltihint['launchid'] = $launchid; 3641 // If SSL is forced make sure https is on the normal launch URL. 3642 if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) { 3643 $endpoint = lti_ensure_url_is_https($endpoint); 3644 } else if (!strstr($endpoint, '://')) { 3645 $endpoint = 'http://' . $endpoint; 3646 } 3647 3648 $params = array(); 3649 $params['iss'] = $CFG->wwwroot; 3650 $params['target_link_uri'] = $endpoint; 3651 $params['login_hint'] = $USER->id; 3652 $params['lti_message_hint'] = json_encode($ltihint); 3653 $params['client_id'] = $config->lti_clientid; 3654 $params['lti_deployment_id'] = $config->typeid; 3655 return $params; 3656 } 3657 3658 function lti_get_type($typeid) { 3659 global $DB; 3660 3661 return $DB->get_record('lti_types', array('id' => $typeid)); 3662 } 3663 3664 function lti_get_launch_container($lti, $toolconfig) { 3665 if (empty($lti->launchcontainer)) { 3666 $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; 3667 } 3668 3669 if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) { 3670 if (isset($toolconfig['launchcontainer'])) { 3671 $launchcontainer = $toolconfig['launchcontainer']; 3672 } 3673 } else { 3674 $launchcontainer = $lti->launchcontainer; 3675 } 3676 3677 if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) { 3678 $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; 3679 } 3680 3681 $devicetype = core_useragent::get_device_type(); 3682 3683 // Scrolling within the object element doesn't work on iOS or Android 3684 // Opening the popup window also had some issues in testing 3685 // For mobile devices, always take up the entire screen to ensure the best experience. 3686 if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) { 3687 $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW; 3688 } 3689 3690 return $launchcontainer; 3691 } 3692 3693 function lti_request_is_using_ssl() { 3694 global $CFG; 3695 return (stripos($CFG->wwwroot, 'https://') === 0); 3696 } 3697 3698 function lti_ensure_url_is_https($url) { 3699 if (!strstr($url, '://')) { 3700 $url = 'https://' . $url; 3701 } else { 3702 // If the URL starts with http, replace with https. 3703 if (stripos($url, 'http://') === 0) { 3704 $url = 'https://' . substr($url, 7); 3705 } 3706 } 3707 3708 return $url; 3709 } 3710 3711 /** 3712 * Determines if we should try to log the request 3713 * 3714 * @param string $rawbody 3715 * @return bool 3716 */ 3717 function lti_should_log_request($rawbody) { 3718 global $CFG; 3719 3720 if (empty($CFG->mod_lti_log_users)) { 3721 return false; 3722 } 3723 3724 $logusers = explode(',', $CFG->mod_lti_log_users); 3725 if (empty($logusers)) { 3726 return false; 3727 } 3728 3729 try { 3730 $xml = new \SimpleXMLElement($rawbody); 3731 $ns = $xml->getNamespaces(); 3732 $ns = array_shift($ns); 3733 $xml->registerXPathNamespace('lti', $ns); 3734 $requestuserid = ''; 3735 if ($node = $xml->xpath('//lti:userId')) { 3736 $node = $node[0]; 3737 $requestuserid = clean_param((string) $node, PARAM_INT); 3738 } else if ($node = $xml->xpath('//lti:sourcedId')) { 3739 $node = $node[0]; 3740 $resultjson = json_decode((string) $node); 3741 $requestuserid = clean_param($resultjson->data->userid, PARAM_INT); 3742 } 3743 } catch (Exception $e) { 3744 return false; 3745 } 3746 3747 if (empty($requestuserid) or !in_array($requestuserid, $logusers)) { 3748 return false; 3749 } 3750 3751 return true; 3752 } 3753 3754 /** 3755 * Logs the request to a file in temp dir. 3756 * 3757 * @param string $rawbody 3758 */ 3759 function lti_log_request($rawbody) { 3760 if ($tempdir = make_temp_directory('mod_lti', false)) { 3761 if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) { 3762 $content = "Request Headers:\n"; 3763 foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) { 3764 $content .= "$header: $value\n"; 3765 } 3766 $content .= "Request Body:\n"; 3767 $content .= $rawbody; 3768 3769 file_put_contents($tempfile, $content); 3770 chmod($tempfile, 0644); 3771 } 3772 } 3773 } 3774 3775 /** 3776 * Log an LTI response. 3777 * 3778 * @param string $responsexml The response XML 3779 * @param Exception $e If there was an exception, pass that too 3780 */ 3781 function lti_log_response($responsexml, $e = null) { 3782 if ($tempdir = make_temp_directory('mod_lti', false)) { 3783 if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) { 3784 $content = ''; 3785 if ($e instanceof Exception) { 3786 $info = get_exception_info($e); 3787 3788 $content .= "Exception:\n"; 3789 $content .= "Message: $info->message\n"; 3790 $content .= "Debug info: $info->debuginfo\n"; 3791 $content .= "Backtrace:\n"; 3792 $content .= format_backtrace($info->backtrace, true); 3793 $content .= "\n"; 3794 } 3795 $content .= "Response XML:\n"; 3796 $content .= $responsexml; 3797 3798 file_put_contents($tempfile, $content); 3799 chmod($tempfile, 0644); 3800 } 3801 } 3802 } 3803 3804 /** 3805 * Fetches LTI type configuration for an LTI instance 3806 * 3807 * @param stdClass $instance 3808 * @return array Can be empty if no type is found 3809 */ 3810 function lti_get_type_config_by_instance($instance) { 3811 $typeid = null; 3812 if (empty($instance->typeid)) { 3813 $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course); 3814 if ($tool) { 3815 $typeid = $tool->id; 3816 } 3817 } else { 3818 $typeid = $instance->typeid; 3819 } 3820 if (!empty($typeid)) { 3821 return lti_get_type_config($typeid); 3822 } 3823 return array(); 3824 } 3825 3826 /** 3827 * Enforce type config settings onto the LTI instance 3828 * 3829 * @param stdClass $instance 3830 * @param array $typeconfig 3831 */ 3832 function lti_force_type_config_settings($instance, array $typeconfig) { 3833 $forced = array( 3834 'instructorchoicesendname' => 'sendname', 3835 'instructorchoicesendemailaddr' => 'sendemailaddr', 3836 'instructorchoiceacceptgrades' => 'acceptgrades', 3837 ); 3838 3839 foreach ($forced as $instanceparam => $typeconfigparam) { 3840 if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) { 3841 $instance->$instanceparam = $typeconfig[$typeconfigparam]; 3842 } 3843 } 3844 } 3845 3846 /** 3847 * Initializes an array with the capabilities supported by the LTI module 3848 * 3849 * @return array List of capability names (without a dollar sign prefix) 3850 */ 3851 function lti_get_capabilities() { 3852 3853 $capabilities = array( 3854 'basic-lti-launch-request' => '', 3855 'ContentItemSelectionRequest' => '', 3856 'ToolProxyRegistrationRequest' => '', 3857 'Context.id' => 'context_id', 3858 'Context.title' => 'context_title', 3859 'Context.label' => 'context_label', 3860 'Context.id.history' => null, 3861 'Context.sourcedId' => 'lis_course_section_sourcedid', 3862 'Context.longDescription' => '$COURSE->summary', 3863 'Context.timeFrame.begin' => '$COURSE->startdate', 3864 'CourseSection.title' => 'context_title', 3865 'CourseSection.label' => 'context_label', 3866 'CourseSection.sourcedId' => 'lis_course_section_sourcedid', 3867 'CourseSection.longDescription' => '$COURSE->summary', 3868 'CourseSection.timeFrame.begin' => null, 3869 'CourseSection.timeFrame.end' => null, 3870 'ResourceLink.id' => 'resource_link_id', 3871 'ResourceLink.title' => 'resource_link_title', 3872 'ResourceLink.description' => 'resource_link_description', 3873 'User.id' => 'user_id', 3874 'User.username' => '$USER->username', 3875 'Person.name.full' => 'lis_person_name_full', 3876 'Person.name.given' => 'lis_person_name_given', 3877 'Person.name.family' => 'lis_person_name_family', 3878 'Person.email.primary' => 'lis_person_contact_email_primary', 3879 'Person.sourcedId' => 'lis_person_sourcedid', 3880 'Person.name.middle' => '$USER->middlename', 3881 'Person.address.street1' => '$USER->address', 3882 'Person.address.locality' => '$USER->city', 3883 'Person.address.country' => '$USER->country', 3884 'Person.address.timezone' => '$USER->timezone', 3885 'Person.phone.primary' => '$USER->phone1', 3886 'Person.phone.mobile' => '$USER->phone2', 3887 'Person.webaddress' => '$USER->url', 3888 'Membership.role' => 'roles', 3889 'Result.sourcedId' => 'lis_result_sourcedid', 3890 'Result.autocreate' => 'lis_outcome_service_url', 3891 'BasicOutcome.sourcedId' => 'lis_result_sourcedid', 3892 'BasicOutcome.url' => 'lis_outcome_service_url', 3893 'Moodle.Person.userGroupIds' => null); 3894 3895 return $capabilities; 3896 3897 } 3898 3899 /** 3900 * Initializes an array with the services supported by the LTI module 3901 * 3902 * @return array List of services 3903 */ 3904 function lti_get_services() { 3905 3906 $services = array(); 3907 $definedservices = core_component::get_plugin_list('ltiservice'); 3908 foreach ($definedservices as $name => $location) { 3909 $classname = "\\ltiservice_{$name}\\local\\service\\{$name}"; 3910 $services[] = new $classname(); 3911 } 3912 3913 return $services; 3914 3915 } 3916 3917 /** 3918 * Initializes an instance of the named service 3919 * 3920 * @param string $servicename Name of service 3921 * 3922 * @return bool|\mod_lti\local\ltiservice\service_base Service 3923 */ 3924 function lti_get_service_by_name($servicename) { 3925 3926 $service = false; 3927 $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}"; 3928 if (class_exists($classname)) { 3929 $service = new $classname(); 3930 } 3931 3932 return $service; 3933 3934 } 3935 3936 /** 3937 * Finds a service by id 3938 * 3939 * @param \mod_lti\local\ltiservice\service_base[] $services Array of services 3940 * @param string $resourceid ID of resource 3941 * 3942 * @return mod_lti\local\ltiservice\service_base Service 3943 */ 3944 function lti_get_service_by_resource_id($services, $resourceid) { 3945 3946 $service = false; 3947 foreach ($services as $aservice) { 3948 foreach ($aservice->get_resources() as $resource) { 3949 if ($resource->get_id() === $resourceid) { 3950 $service = $aservice; 3951 break 2; 3952 } 3953 } 3954 } 3955 3956 return $service; 3957 3958 } 3959 3960 /** 3961 * Initializes an array with the scopes for services supported by the LTI module 3962 * and authorized for this particular tool instance. 3963 * 3964 * @param object $type LTI tool type 3965 * @param array $typeconfig LTI tool type configuration 3966 * 3967 * @return array List of scopes 3968 */ 3969 function lti_get_permitted_service_scopes($type, $typeconfig) { 3970 3971 $services = lti_get_services(); 3972 $scopes = array(); 3973 foreach ($services as $service) { 3974 $service->set_type($type); 3975 $service->set_typeconfig($typeconfig); 3976 $servicescopes = $service->get_permitted_scopes(); 3977 if (!empty($servicescopes)) { 3978 $scopes = array_merge($scopes, $servicescopes); 3979 } 3980 } 3981 3982 return $scopes; 3983 } 3984 3985 /** 3986 * Extracts the named contexts from a tool proxy 3987 * 3988 * @param object $json 3989 * 3990 * @return array Contexts 3991 */ 3992 function lti_get_contexts($json) { 3993 3994 $contexts = array(); 3995 if (isset($json->{'@context'})) { 3996 foreach ($json->{'@context'} as $context) { 3997 if (is_object($context)) { 3998 $contexts = array_merge(get_object_vars($context), $contexts); 3999 } 4000 } 4001 } 4002 4003 return $contexts; 4004 4005 } 4006 4007 /** 4008 * Converts an ID to a fully-qualified ID 4009 * 4010 * @param array $contexts 4011 * @param string $id 4012 * 4013 * @return string Fully-qualified ID 4014 */ 4015 function lti_get_fqid($contexts, $id) { 4016 4017 $parts = explode(':', $id, 2); 4018 if (count($parts) > 1) { 4019 if (array_key_exists($parts[0], $contexts)) { 4020 $id = $contexts[$parts[0]] . $parts[1]; 4021 } 4022 } 4023 4024 return $id; 4025 4026 } 4027 4028 /** 4029 * Returns the icon for the given tool type 4030 * 4031 * @param stdClass $type The tool type 4032 * 4033 * @return string The url to the tool type's corresponding icon 4034 */ 4035 function get_tool_type_icon_url(stdClass $type) { 4036 global $OUTPUT; 4037 4038 $iconurl = $type->secureicon; 4039 4040 if (empty($iconurl)) { 4041 $iconurl = $type->icon; 4042 } 4043 4044 if (empty($iconurl)) { 4045 $iconurl = $OUTPUT->image_url('monologo', 'lti')->out(); 4046 } 4047 4048 return $iconurl; 4049 } 4050 4051 /** 4052 * Returns the edit url for the given tool type 4053 * 4054 * @param stdClass $type The tool type 4055 * 4056 * @return string The url to edit the tool type 4057 */ 4058 function get_tool_type_edit_url(stdClass $type) { 4059 $url = new moodle_url('/mod/lti/typessettings.php', 4060 array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure')); 4061 return $url->out(); 4062 } 4063 4064 /** 4065 * Returns the edit url for the given tool proxy. 4066 * 4067 * @param stdClass $proxy The tool proxy 4068 * 4069 * @return string The url to edit the tool type 4070 */ 4071 function get_tool_proxy_edit_url(stdClass $proxy) { 4072 $url = new moodle_url('/mod/lti/registersettings.php', 4073 array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure')); 4074 return $url->out(); 4075 } 4076 4077 /** 4078 * Returns the course url for the given tool type 4079 * 4080 * @param stdClass $type The tool type 4081 * 4082 * @return string The url to the course of the tool type, void if it is a site wide type 4083 */ 4084 function get_tool_type_course_url(stdClass $type) { 4085 if ($type->course != 1) { 4086 $url = new moodle_url('/course/view.php', array('id' => $type->course)); 4087 return $url->out(); 4088 } 4089 return null; 4090 } 4091 4092 /** 4093 * Returns the icon and edit urls for the tool type and the course url if it is a course type. 4094 * 4095 * @param stdClass $type The tool type 4096 * 4097 * @return array The urls of the tool type 4098 */ 4099 function get_tool_type_urls(stdClass $type) { 4100 $courseurl = get_tool_type_course_url($type); 4101 4102 $urls = array( 4103 'icon' => get_tool_type_icon_url($type), 4104 'edit' => get_tool_type_edit_url($type), 4105 ); 4106 4107 if ($courseurl) { 4108 $urls['course'] = $courseurl; 4109 } 4110 4111 $url = new moodle_url('/mod/lti/certs.php'); 4112 $urls['publickeyset'] = $url->out(); 4113 $url = new moodle_url('/mod/lti/token.php'); 4114 $urls['accesstoken'] = $url->out(); 4115 $url = new moodle_url('/mod/lti/auth.php'); 4116 $urls['authrequest'] = $url->out(); 4117 4118 return $urls; 4119 } 4120 4121 /** 4122 * Returns the icon and edit urls for the tool proxy. 4123 * 4124 * @param stdClass $proxy The tool proxy 4125 * 4126 * @return array The urls of the tool proxy 4127 */ 4128 function get_tool_proxy_urls(stdClass $proxy) { 4129 global $OUTPUT; 4130 4131 $urls = array( 4132 'icon' => $OUTPUT->image_url('monologo', 'lti')->out(), 4133 'edit' => get_tool_proxy_edit_url($proxy), 4134 ); 4135 4136 return $urls; 4137 } 4138 4139 /** 4140 * Returns information on the current state of the tool type 4141 * 4142 * @param stdClass $type The tool type 4143 * 4144 * @return array An array with a text description of the state, and boolean for whether it is in each state: 4145 * pending, configured, rejected, unknown 4146 */ 4147 function get_tool_type_state_info(stdClass $type) { 4148 $isconfigured = false; 4149 $ispending = false; 4150 $isrejected = false; 4151 $isunknown = false; 4152 switch ($type->state) { 4153 case LTI_TOOL_STATE_CONFIGURED: 4154 $state = get_string('active', 'mod_lti'); 4155 $isconfigured = true; 4156 break; 4157 case LTI_TOOL_STATE_PENDING: 4158 $state = get_string('pending', 'mod_lti'); 4159 $ispending = true; 4160 break; 4161 case LTI_TOOL_STATE_REJECTED: 4162 $state = get_string('rejected', 'mod_lti'); 4163 $isrejected = true; 4164 break; 4165 default: 4166 $state = get_string('unknownstate', 'mod_lti'); 4167 $isunknown = true; 4168 break; 4169 } 4170 4171 return array( 4172 'text' => $state, 4173 'pending' => $ispending, 4174 'configured' => $isconfigured, 4175 'rejected' => $isrejected, 4176 'unknown' => $isunknown 4177 ); 4178 } 4179 4180 /** 4181 * Returns information on the configuration of the tool type 4182 * 4183 * @param stdClass $type The tool type 4184 * 4185 * @return array An array with configuration details 4186 */ 4187 function get_tool_type_config($type) { 4188 global $CFG; 4189 $platformid = $CFG->wwwroot; 4190 $clientid = $type->clientid; 4191 $deploymentid = $type->id; 4192 $publickeyseturl = new moodle_url('/mod/lti/certs.php'); 4193 $publickeyseturl = $publickeyseturl->out(); 4194 4195 $accesstokenurl = new moodle_url('/mod/lti/token.php'); 4196 $accesstokenurl = $accesstokenurl->out(); 4197 4198 $authrequesturl = new moodle_url('/mod/lti/auth.php'); 4199 $authrequesturl = $authrequesturl->out(); 4200 4201 return array( 4202 'platformid' => $platformid, 4203 'clientid' => $clientid, 4204 'deploymentid' => $deploymentid, 4205 'publickeyseturl' => $publickeyseturl, 4206 'accesstokenurl' => $accesstokenurl, 4207 'authrequesturl' => $authrequesturl 4208 ); 4209 } 4210 4211 /** 4212 * Returns a summary of each LTI capability this tool type requires in plain language 4213 * 4214 * @param stdClass $type The tool type 4215 * 4216 * @return array An array of text descriptions of each of the capabilities this tool type requires 4217 */ 4218 function get_tool_type_capability_groups($type) { 4219 $capabilities = lti_get_enabled_capabilities($type); 4220 $groups = array(); 4221 $hascourse = false; 4222 $hasactivities = false; 4223 $hasuseraccount = false; 4224 $hasuserpersonal = false; 4225 4226 foreach ($capabilities as $capability) { 4227 // Bail out early if we've already found all groups. 4228 if (count($groups) >= 4) { 4229 continue; 4230 } 4231 4232 if (!$hascourse && preg_match('/^CourseSection/', $capability)) { 4233 $hascourse = true; 4234 $groups[] = get_string('courseinformation', 'mod_lti'); 4235 } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) { 4236 $hasactivities = true; 4237 $groups[] = get_string('courseactivitiesorresources', 'mod_lti'); 4238 } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) { 4239 $hasuseraccount = true; 4240 $groups[] = get_string('useraccountinformation', 'mod_lti'); 4241 } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) { 4242 $hasuserpersonal = true; 4243 $groups[] = get_string('userpersonalinformation', 'mod_lti'); 4244 } 4245 } 4246 4247 return $groups; 4248 } 4249 4250 4251 /** 4252 * Returns the ids of each instance of this tool type 4253 * 4254 * @param stdClass $type The tool type 4255 * 4256 * @return array An array of ids of the instances of this tool type 4257 */ 4258 function get_tool_type_instance_ids($type) { 4259 global $DB; 4260 4261 return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id))); 4262 } 4263 4264 /** 4265 * Serialises this tool type 4266 * 4267 * @param stdClass $type The tool type 4268 * 4269 * @return array An array of values representing this type 4270 */ 4271 function serialise_tool_type(stdClass $type) { 4272 global $CFG; 4273 4274 $capabilitygroups = get_tool_type_capability_groups($type); 4275 $instanceids = get_tool_type_instance_ids($type); 4276 // Clean the name. We don't want tags here. 4277 $name = clean_param($type->name, PARAM_NOTAGS); 4278 if (!empty($type->description)) { 4279 // Clean the description. We don't want tags here. 4280 $description = clean_param($type->description, PARAM_NOTAGS); 4281 } else { 4282 $description = get_string('editdescription', 'mod_lti'); 4283 } 4284 return array( 4285 'id' => $type->id, 4286 'name' => $name, 4287 'description' => $description, 4288 'urls' => get_tool_type_urls($type), 4289 'state' => get_tool_type_state_info($type), 4290 'platformid' => $CFG->wwwroot, 4291 'clientid' => $type->clientid, 4292 'deploymentid' => $type->id, 4293 'hascapabilitygroups' => !empty($capabilitygroups), 4294 'capabilitygroups' => $capabilitygroups, 4295 // Course ID of 1 means it's not linked to a course. 4296 'courseid' => $type->course == 1 ? 0 : $type->course, 4297 'instanceids' => $instanceids, 4298 'instancecount' => count($instanceids) 4299 ); 4300 } 4301 4302 /** 4303 * Serialises this tool proxy. 4304 * 4305 * @param stdClass $proxy The tool proxy 4306 * 4307 * @deprecated since Moodle 3.10 4308 * @todo This will be finally removed for Moodle 4.2 as part of MDL-69976. 4309 * @return array An array of values representing this type 4310 */ 4311 function serialise_tool_proxy(stdClass $proxy) { 4312 $deprecatedtext = __FUNCTION__ . '() is deprecated. Please remove all references to this method.'; 4313 debugging($deprecatedtext, DEBUG_DEVELOPER); 4314 4315 return array( 4316 'id' => $proxy->id, 4317 'name' => $proxy->name, 4318 'description' => get_string('activatetoadddescription', 'mod_lti'), 4319 'urls' => get_tool_proxy_urls($proxy), 4320 'state' => array( 4321 'text' => get_string('pending', 'mod_lti'), 4322 'pending' => true, 4323 'configured' => false, 4324 'rejected' => false, 4325 'unknown' => false 4326 ), 4327 'hascapabilitygroups' => true, 4328 'capabilitygroups' => array(), 4329 'courseid' => 0, 4330 'instanceids' => array(), 4331 'instancecount' => 0 4332 ); 4333 } 4334 4335 /** 4336 * Loads the cartridge information into the tool type, if the launch url is for a cartridge file 4337 * 4338 * @param stdClass $type The tool type object to be filled in 4339 * @since Moodle 3.1 4340 */ 4341 function lti_load_type_if_cartridge($type) { 4342 if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) { 4343 lti_load_type_from_cartridge($type->lti_toolurl, $type); 4344 } 4345 } 4346 4347 /** 4348 * Loads the cartridge information into the new tool, if the launch url is for a cartridge file 4349 * 4350 * @param stdClass $lti The tools config 4351 * @since Moodle 3.1 4352 */ 4353 function lti_load_tool_if_cartridge($lti) { 4354 if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) { 4355 lti_load_tool_from_cartridge($lti->toolurl, $lti); 4356 } 4357 } 4358 4359 /** 4360 * Determines if the given url is for a IMS basic cartridge 4361 * 4362 * @param string $url The url to be checked 4363 * @return True if the url is for a cartridge 4364 * @since Moodle 3.1 4365 */ 4366 function lti_is_cartridge($url) { 4367 // If it is empty, it's not a cartridge. 4368 if (empty($url)) { 4369 return false; 4370 } 4371 // If it has xml at the end of the url, it's a cartridge. 4372 if (preg_match('/\.xml$/', $url)) { 4373 return true; 4374 } 4375 // Even if it doesn't have .xml, load the url to check if it's a cartridge.. 4376 try { 4377 $toolinfo = lti_load_cartridge($url, 4378 array( 4379 "launch_url" => "launchurl" 4380 ) 4381 ); 4382 if (!empty($toolinfo['launchurl'])) { 4383 return true; 4384 } 4385 } catch (moodle_exception $e) { 4386 return false; // Error loading the xml, so it's not a cartridge. 4387 } 4388 return false; 4389 } 4390 4391 /** 4392 * Allows you to load settings for an external tool type from an IMS cartridge. 4393 * 4394 * @param string $url The URL to the cartridge 4395 * @param stdClass $type The tool type object to be filled in 4396 * @throws moodle_exception if the cartridge could not be loaded correctly 4397 * @since Moodle 3.1 4398 */ 4399 function lti_load_type_from_cartridge($url, $type) { 4400 $toolinfo = lti_load_cartridge($url, 4401 array( 4402 "title" => "lti_typename", 4403 "launch_url" => "lti_toolurl", 4404 "description" => "lti_description", 4405 "icon" => "lti_icon", 4406 "secure_icon" => "lti_secureicon" 4407 ), 4408 array( 4409 "icon_url" => "lti_extension_icon", 4410 "secure_icon_url" => "lti_extension_secureicon" 4411 ) 4412 ); 4413 // If an activity name exists, unset the cartridge name so we don't override it. 4414 if (isset($type->lti_typename)) { 4415 unset($toolinfo['lti_typename']); 4416 } 4417 4418 // Always prefer cartridge core icons first, then, if none are found, look at the extension icons. 4419 if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) { 4420 $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon']; 4421 } 4422 unset($toolinfo['lti_extension_icon']); 4423 4424 if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) { 4425 $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon']; 4426 } 4427 unset($toolinfo['lti_extension_secureicon']); 4428 4429 // Ensure Custom icons aren't overridden by cartridge params. 4430 if (!empty($type->lti_icon)) { 4431 unset($toolinfo['lti_icon']); 4432 } 4433 4434 if (!empty($type->lti_secureicon)) { 4435 unset($toolinfo['lti_secureicon']); 4436 } 4437 4438 foreach ($toolinfo as $property => $value) { 4439 $type->$property = $value; 4440 } 4441 } 4442 4443 /** 4444 * Allows you to load in the configuration for an external tool from an IMS cartridge. 4445 * 4446 * @param string $url The URL to the cartridge 4447 * @param stdClass $lti LTI object 4448 * @throws moodle_exception if the cartridge could not be loaded correctly 4449 * @since Moodle 3.1 4450 */ 4451 function lti_load_tool_from_cartridge($url, $lti) { 4452 $toolinfo = lti_load_cartridge($url, 4453 array( 4454 "title" => "name", 4455 "launch_url" => "toolurl", 4456 "secure_launch_url" => "securetoolurl", 4457 "description" => "intro", 4458 "icon" => "icon", 4459 "secure_icon" => "secureicon" 4460 ), 4461 array( 4462 "icon_url" => "extension_icon", 4463 "secure_icon_url" => "extension_secureicon" 4464 ) 4465 ); 4466 // If an activity name exists, unset the cartridge name so we don't override it. 4467 if (isset($lti->name)) { 4468 unset($toolinfo['name']); 4469 } 4470 4471 // Always prefer cartridge core icons first, then, if none are found, look at the extension icons. 4472 if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) { 4473 $toolinfo['icon'] = $toolinfo['extension_icon']; 4474 } 4475 unset($toolinfo['extension_icon']); 4476 4477 if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) { 4478 $toolinfo['secureicon'] = $toolinfo['extension_secureicon']; 4479 } 4480 unset($toolinfo['extension_secureicon']); 4481 4482 foreach ($toolinfo as $property => $value) { 4483 $lti->$property = $value; 4484 } 4485 } 4486 4487 /** 4488 * Search for a tag within an XML DOMDocument 4489 * 4490 * @param string $url The url of the cartridge to be loaded 4491 * @param array $map The map of tags to keys in the return array 4492 * @param array $propertiesmap The map of properties to keys in the return array 4493 * @return array An associative array with the given keys and their values from the cartridge 4494 * @throws moodle_exception if the cartridge could not be loaded correctly 4495 * @since Moodle 3.1 4496 */ 4497 function lti_load_cartridge($url, $map, $propertiesmap = array()) { 4498 global $CFG; 4499 require_once($CFG->libdir. "/filelib.php"); 4500 4501 $curl = new curl(); 4502 $response = $curl->get($url); 4503 4504 // Got a completely empty response (real or error), cannot process this with 4505 // DOMDocument::loadXML() because it errors with ValueError. So let's throw 4506 // the moodle_exception before waiting to examine the errors later. 4507 if (trim($response) === '') { 4508 throw new moodle_exception('errorreadingfile', '', '', $url); 4509 } 4510 4511 // TODO MDL-46023 Replace this code with a call to the new library. 4512 $origerrors = libxml_use_internal_errors(true); 4513 $origentity = lti_libxml_disable_entity_loader(true); 4514 libxml_clear_errors(); 4515 4516 $document = new DOMDocument(); 4517 @$document->loadXML($response, LIBXML_NONET); 4518 4519 $cartridge = new DomXpath($document); 4520 4521 $errors = libxml_get_errors(); 4522 4523 libxml_clear_errors(); 4524 libxml_use_internal_errors($origerrors); 4525 lti_libxml_disable_entity_loader($origentity); 4526 4527 if (count($errors) > 0) { 4528 $message = 'Failed to load cartridge.'; 4529 foreach ($errors as $error) { 4530 $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line; 4531 } 4532 throw new moodle_exception('errorreadingfile', '', '', $url, $message); 4533 } 4534 4535 $toolinfo = array(); 4536 foreach ($map as $tag => $key) { 4537 $value = get_tag($tag, $cartridge); 4538 if ($value) { 4539 $toolinfo[$key] = $value; 4540 } 4541 } 4542 if (!empty($propertiesmap)) { 4543 foreach ($propertiesmap as $property => $key) { 4544 $value = get_tag("property", $cartridge, $property); 4545 if ($value) { 4546 $toolinfo[$key] = $value; 4547 } 4548 } 4549 } 4550 4551 return $toolinfo; 4552 } 4553 4554 /** 4555 * Search for a tag within an XML DOMDocument 4556 * 4557 * @param stdClass $tagname The name of the tag to search for 4558 * @param XPath $xpath The XML to find the tag in 4559 * @param XPath $attribute The attribute to search for (if we should search for a child node with the given 4560 * value for the name attribute 4561 * @since Moodle 3.1 4562 */ 4563 function get_tag($tagname, $xpath, $attribute = null) { 4564 if ($attribute) { 4565 $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]'); 4566 } else { 4567 $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']'); 4568 } 4569 if ($result->length > 0) { 4570 return $result->item(0)->nodeValue; 4571 } 4572 return null; 4573 } 4574 4575 /** 4576 * Create a new access token. 4577 * 4578 * @param int $typeid Tool type ID 4579 * @param string[] $scopes Scopes permitted for new token 4580 * 4581 * @return stdClass Access token 4582 */ 4583 function lti_new_access_token($typeid, $scopes) { 4584 global $DB; 4585 4586 // Make sure the token doesn't exist (even if it should be almost impossible with the random generation). 4587 $numtries = 0; 4588 do { 4589 $numtries ++; 4590 $generatedtoken = md5(uniqid(rand(), 1)); 4591 if ($numtries > 5) { 4592 throw new moodle_exception('Failed to generate LTI access token'); 4593 } 4594 } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken))); 4595 $newtoken = new stdClass(); 4596 $newtoken->typeid = $typeid; 4597 $newtoken->scope = json_encode(array_values($scopes)); 4598 $newtoken->token = $generatedtoken; 4599 4600 $newtoken->timecreated = time(); 4601 $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE; 4602 $newtoken->lastaccess = null; 4603 4604 $DB->insert_record('lti_access_tokens', $newtoken); 4605 4606 return $newtoken; 4607 4608 } 4609 4610 4611 /** 4612 * Wrapper for function libxml_disable_entity_loader() deprecated in PHP 8 4613 * 4614 * Method was deprecated in PHP 8 and it shows deprecation message. However it is still 4615 * required in the previous versions on PHP. While Moodle supports both PHP 7 and 8 we need to keep it. 4616 * @see https://php.watch/versions/8.0/libxml_disable_entity_loader-deprecation 4617 * 4618 * @param bool $value 4619 * @return bool 4620 */ 4621 function lti_libxml_disable_entity_loader(bool $value): bool { 4622 if (PHP_VERSION_ID < 80000) { 4623 return (bool)libxml_disable_entity_loader($value); 4624 } 4625 return true; 4626 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body