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