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