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