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