MDL-78251 tiny: Reduce use of UI for setting test expectations
[moodle.git] / mod / lti / locallib.php
blob40f7fdc5b091f5c3c1a28a557cbaed57b6fb986a
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.
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/>.
17 // This file is part of BasicLTI4Moodle
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.
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.
35 /**
36 * This file contains the library of functions and constants for the lti module
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
51 defined('MOODLE_INTERNAL') || die;
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;
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');
68 define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i');
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);
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);
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);
87 define('LTI_SETTING_NEVER', 0);
88 define('LTI_SETTING_ALWAYS', 1);
89 define('LTI_SETTING_DELEGATE', 2);
91 define('LTI_COURSEVISIBLE_NO', 0);
92 define('LTI_COURSEVISIBLE_PRECONFIGURED', 1);
93 define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2);
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');
101 define('LTI_DEFAULT_ORGID_SITEID', 'SITEID');
102 define('LTI_DEFAULT_ORGID_SITEHOST', 'SITEHOST');
104 define('LTI_ACCESS_TOKEN_LIFE', 3600);
106 // Standard prefix for JWT claims.
107 define('LTI_JWT_CLAIM_PREFIX', 'https://purl.imsglobal.org/spec/lti');
110 * Return the mapping for standard message types to JWT message_type claim.
112 * @return array
114 function lti_get_jwt_message_type_mapping() {
115 return array(
116 'basic-lti-launch-request' => 'LtiResourceLinkRequest',
117 'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest',
118 'LtiDeepLinkingResponse' => 'ContentItemSelection',
119 'LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest',
124 * Return the mapping for standard message parameters to JWT claim.
126 * @return array
128 function lti_get_jwt_claim_mapping() {
129 $mapping = [];
130 $services = lti_get_services();
131 foreach ($services as $service) {
132 $mapping = array_merge($mapping, $service->get_jwt_claim_mappings());
134 $mapping = array_merge($mapping, [
135 'accept_copy_advice' => [
136 'suffix' => 'dl',
137 'group' => 'deep_linking_settings',
138 'claim' => 'accept_copy_advice',
139 'isarray' => false,
140 'type' => 'boolean'
142 'accept_media_types' => [
143 'suffix' => 'dl',
144 'group' => 'deep_linking_settings',
145 'claim' => 'accept_media_types',
146 'isarray' => true
148 'accept_multiple' => [
149 'suffix' => 'dl',
150 'group' => 'deep_linking_settings',
151 'claim' => 'accept_multiple',
152 'isarray' => false,
153 'type' => 'boolean'
155 'accept_presentation_document_targets' => [
156 'suffix' => 'dl',
157 'group' => 'deep_linking_settings',
158 'claim' => 'accept_presentation_document_targets',
159 'isarray' => true
161 'accept_types' => [
162 'suffix' => 'dl',
163 'group' => 'deep_linking_settings',
164 'claim' => 'accept_types',
165 'isarray' => true
167 'accept_unsigned' => [
168 'suffix' => 'dl',
169 'group' => 'deep_linking_settings',
170 'claim' => 'accept_unsigned',
171 'isarray' => false,
172 'type' => 'boolean'
174 'auto_create' => [
175 'suffix' => 'dl',
176 'group' => 'deep_linking_settings',
177 'claim' => 'auto_create',
178 'isarray' => false,
179 'type' => 'boolean'
181 'can_confirm' => [
182 'suffix' => 'dl',
183 'group' => 'deep_linking_settings',
184 'claim' => 'can_confirm',
185 'isarray' => false,
186 'type' => 'boolean'
188 'content_item_return_url' => [
189 'suffix' => 'dl',
190 'group' => 'deep_linking_settings',
191 'claim' => 'deep_link_return_url',
192 'isarray' => false
194 'content_items' => [
195 'suffix' => 'dl',
196 'group' => '',
197 'claim' => 'content_items',
198 'isarray' => true
200 'data' => [
201 'suffix' => 'dl',
202 'group' => 'deep_linking_settings',
203 'claim' => 'data',
204 'isarray' => false
206 'text' => [
207 'suffix' => 'dl',
208 'group' => 'deep_linking_settings',
209 'claim' => 'text',
210 'isarray' => false
212 'title' => [
213 'suffix' => 'dl',
214 'group' => 'deep_linking_settings',
215 'claim' => 'title',
216 'isarray' => false
218 'lti_msg' => [
219 'suffix' => 'dl',
220 'group' => '',
221 'claim' => 'msg',
222 'isarray' => false
224 'lti_log' => [
225 'suffix' => 'dl',
226 'group' => '',
227 'claim' => 'log',
228 'isarray' => false
230 'lti_errormsg' => [
231 'suffix' => 'dl',
232 'group' => '',
233 'claim' => 'errormsg',
234 'isarray' => false
236 'lti_errorlog' => [
237 'suffix' => 'dl',
238 'group' => '',
239 'claim' => 'errorlog',
240 'isarray' => false
242 'context_id' => [
243 'suffix' => '',
244 'group' => 'context',
245 'claim' => 'id',
246 'isarray' => false
248 'context_label' => [
249 'suffix' => '',
250 'group' => 'context',
251 'claim' => 'label',
252 'isarray' => false
254 'context_title' => [
255 'suffix' => '',
256 'group' => 'context',
257 'claim' => 'title',
258 'isarray' => false
260 'context_type' => [
261 'suffix' => '',
262 'group' => 'context',
263 'claim' => 'type',
264 'isarray' => true
266 'for_user_id' => [
267 'suffix' => '',
268 'group' => 'for_user',
269 'claim' => 'user_id',
270 'isarray' => false
272 'lis_course_offering_sourcedid' => [
273 'suffix' => '',
274 'group' => 'lis',
275 'claim' => 'course_offering_sourcedid',
276 'isarray' => false
278 'lis_course_section_sourcedid' => [
279 'suffix' => '',
280 'group' => 'lis',
281 'claim' => 'course_section_sourcedid',
282 'isarray' => false
284 'launch_presentation_css_url' => [
285 'suffix' => '',
286 'group' => 'launch_presentation',
287 'claim' => 'css_url',
288 'isarray' => false
290 'launch_presentation_document_target' => [
291 'suffix' => '',
292 'group' => 'launch_presentation',
293 'claim' => 'document_target',
294 'isarray' => false
296 'launch_presentation_height' => [
297 'suffix' => '',
298 'group' => 'launch_presentation',
299 'claim' => 'height',
300 'isarray' => false
302 'launch_presentation_locale' => [
303 'suffix' => '',
304 'group' => 'launch_presentation',
305 'claim' => 'locale',
306 'isarray' => false
308 'launch_presentation_return_url' => [
309 'suffix' => '',
310 'group' => 'launch_presentation',
311 'claim' => 'return_url',
312 'isarray' => false
314 'launch_presentation_width' => [
315 'suffix' => '',
316 'group' => 'launch_presentation',
317 'claim' => 'width',
318 'isarray' => false
320 'lis_person_contact_email_primary' => [
321 'suffix' => '',
322 'group' => null,
323 'claim' => 'email',
324 'isarray' => false
326 'lis_person_name_family' => [
327 'suffix' => '',
328 'group' => null,
329 'claim' => 'family_name',
330 'isarray' => false
332 'lis_person_name_full' => [
333 'suffix' => '',
334 'group' => null,
335 'claim' => 'name',
336 'isarray' => false
338 'lis_person_name_given' => [
339 'suffix' => '',
340 'group' => null,
341 'claim' => 'given_name',
342 'isarray' => false
344 'lis_person_sourcedid' => [
345 'suffix' => '',
346 'group' => 'lis',
347 'claim' => 'person_sourcedid',
348 'isarray' => false
350 'user_id' => [
351 'suffix' => '',
352 'group' => null,
353 'claim' => 'sub',
354 'isarray' => false
356 'user_image' => [
357 'suffix' => '',
358 'group' => null,
359 'claim' => 'picture',
360 'isarray' => false
362 'roles' => [
363 'suffix' => '',
364 'group' => '',
365 'claim' => 'roles',
366 'isarray' => true
368 'role_scope_mentor' => [
369 'suffix' => '',
370 'group' => '',
371 'claim' => 'role_scope_mentor',
372 'isarray' => false
374 'deployment_id' => [
375 'suffix' => '',
376 'group' => '',
377 'claim' => 'deployment_id',
378 'isarray' => false
380 'lti_message_type' => [
381 'suffix' => '',
382 'group' => '',
383 'claim' => 'message_type',
384 'isarray' => false
386 'lti_version' => [
387 'suffix' => '',
388 'group' => '',
389 'claim' => 'version',
390 'isarray' => false
392 'resource_link_description' => [
393 'suffix' => '',
394 'group' => 'resource_link',
395 'claim' => 'description',
396 'isarray' => false
398 'resource_link_id' => [
399 'suffix' => '',
400 'group' => 'resource_link',
401 'claim' => 'id',
402 'isarray' => false
404 'resource_link_title' => [
405 'suffix' => '',
406 'group' => 'resource_link',
407 'claim' => 'title',
408 'isarray' => false
410 'tool_consumer_info_product_family_code' => [
411 'suffix' => '',
412 'group' => 'tool_platform',
413 'claim' => 'product_family_code',
414 'isarray' => false
416 'tool_consumer_info_version' => [
417 'suffix' => '',
418 'group' => 'tool_platform',
419 'claim' => 'version',
420 'isarray' => false
422 'tool_consumer_instance_contact_email' => [
423 'suffix' => '',
424 'group' => 'tool_platform',
425 'claim' => 'contact_email',
426 'isarray' => false
428 'tool_consumer_instance_description' => [
429 'suffix' => '',
430 'group' => 'tool_platform',
431 'claim' => 'description',
432 'isarray' => false
434 'tool_consumer_instance_guid' => [
435 'suffix' => '',
436 'group' => 'tool_platform',
437 'claim' => 'guid',
438 'isarray' => false
440 'tool_consumer_instance_name' => [
441 'suffix' => '',
442 'group' => 'tool_platform',
443 'claim' => 'name',
444 'isarray' => false
446 'tool_consumer_instance_url' => [
447 'suffix' => '',
448 'group' => 'tool_platform',
449 'claim' => 'url',
450 'isarray' => false
453 return $mapping;
457 * Return the type of the instance, using domain matching if no explicit type is set.
459 * @param object $instance the external tool activity settings
460 * @return object|null
461 * @since Moodle 3.9
463 function lti_get_instance_type(object $instance) : ?object {
464 if (empty($instance->typeid)) {
465 if (!$tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course)) {
466 $tool = lti_get_tool_by_url_match($instance->securetoolurl, $instance->course);
468 return $tool;
470 return lti_get_type($instance->typeid);
474 * Return the launch data required for opening the external tool.
476 * @param stdClass $instance the external tool activity settings
477 * @param string $nonce the nonce value to use (applies to LTI 1.3 only)
478 * @return array the endpoint URL and parameters (including the signature)
479 * @since Moodle 3.0
481 function lti_get_launch_data($instance, $nonce = '', $messagetype = 'basic-lti-launch-request', $foruserid = 0) {
482 global $PAGE, $USER;
483 $messagetype = $messagetype ? $messagetype : 'basic-lti-launch-request';
484 $tool = lti_get_instance_type($instance);
485 if ($tool) {
486 $typeid = $tool->id;
487 $ltiversion = $tool->ltiversion;
488 } else {
489 $typeid = null;
490 $ltiversion = LTI_VERSION_1;
493 if ($typeid) {
494 $typeconfig = lti_get_type_config($typeid);
495 } else {
496 // There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults.
497 $typeconfig = (array)$instance;
499 $typeconfig['sendname'] = $instance->instructorchoicesendname;
500 $typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr;
501 $typeconfig['customparameters'] = $instance->instructorcustomparameters;
502 $typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades;
503 $typeconfig['allowroster'] = $instance->instructorchoiceallowroster;
504 $typeconfig['forcessl'] = '0';
507 if (isset($tool->toolproxyid)) {
508 $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
509 $key = $toolproxy->guid;
510 $secret = $toolproxy->secret;
511 } else {
512 $toolproxy = null;
513 if (!empty($instance->resourcekey)) {
514 $key = $instance->resourcekey;
515 } else if ($ltiversion === LTI_VERSION_1P3) {
516 $key = $tool->clientid;
517 } else if (!empty($typeconfig['resourcekey'])) {
518 $key = $typeconfig['resourcekey'];
519 } else {
520 $key = '';
522 if (!empty($instance->password)) {
523 $secret = $instance->password;
524 } else if (!empty($typeconfig['password'])) {
525 $secret = $typeconfig['password'];
526 } else {
527 $secret = '';
531 $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl'];
532 $endpoint = trim($endpoint);
534 // If the current request is using SSL and a secure tool URL is specified, use it.
535 if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) {
536 $endpoint = trim($instance->securetoolurl);
539 // If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL.
540 if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
541 if (!empty($instance->securetoolurl)) {
542 $endpoint = trim($instance->securetoolurl);
545 if ($endpoint !== '') {
546 $endpoint = lti_ensure_url_is_https($endpoint);
548 } else if ($endpoint !== '' && !strstr($endpoint, '://')) {
549 $endpoint = 'http://' . $endpoint;
552 $orgid = lti_get_organizationid($typeconfig);
554 $course = $PAGE->course;
555 $islti2 = isset($tool->toolproxyid);
556 $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2, $messagetype, $foruserid);
557 if ($islti2) {
558 $requestparams = lti_build_request_lti2($tool, $allparams);
559 } else {
560 $requestparams = $allparams;
562 $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype));
563 $customstr = '';
564 if (isset($typeconfig['customparameters'])) {
565 $customstr = $typeconfig['customparameters'];
567 $services = lti_get_services();
568 foreach ($services as $service) {
569 [$endpoint, $customstr] = $service->override_endpoint($messagetype,
570 $endpoint, $customstr, $instance->course, $instance);
572 $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr,
573 $instance->instructorcustomparameters, $islti2));
575 $launchcontainer = lti_get_launch_container($instance, $typeconfig);
576 $returnurlparams = array('course' => $course->id,
577 'launch_container' => $launchcontainer,
578 'instanceid' => $instance->id,
579 'sesskey' => sesskey());
581 // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns.
582 $url = new \moodle_url('/mod/lti/return.php', $returnurlparams);
583 $returnurl = $url->out(false);
585 if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
586 $returnurl = lti_ensure_url_is_https($returnurl);
589 $target = '';
590 switch($launchcontainer) {
591 case LTI_LAUNCH_CONTAINER_EMBED:
592 case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS:
593 $target = 'iframe';
594 break;
595 case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW:
596 $target = 'frame';
597 break;
598 case LTI_LAUNCH_CONTAINER_WINDOW:
599 $target = 'window';
600 break;
602 if (!empty($target)) {
603 $requestparams['launch_presentation_document_target'] = $target;
606 $requestparams['launch_presentation_return_url'] = $returnurl;
608 // Add the parameters configured by the LTI services.
609 if ($typeid && !$islti2) {
610 $services = lti_get_services();
611 foreach ($services as $service) {
612 $serviceparameters = $service->get_launch_parameters('basic-lti-launch-request',
613 $course->id, $USER->id , $typeid, $instance->id);
614 foreach ($serviceparameters as $paramkey => $paramvalue) {
615 $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
616 $islti2);
621 // Allow request params to be updated by sub-plugins.
622 $plugins = core_component::get_plugin_list('ltisource');
623 foreach (array_keys($plugins) as $plugin) {
624 $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch',
625 array($instance, $endpoint, $requestparams), array());
627 if (!empty($pluginparams) && is_array($pluginparams)) {
628 $requestparams = array_merge($requestparams, $pluginparams);
632 if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) {
633 if ($ltiversion !== LTI_VERSION_1P3) {
634 $parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret);
635 } else {
636 $parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce);
639 $endpointurl = new \moodle_url($endpoint);
640 $endpointparams = $endpointurl->params();
642 // Strip querystring params in endpoint url from $parms to avoid duplication.
643 if (!empty($endpointparams) && !empty($parms)) {
644 foreach (array_keys($endpointparams) as $paramname) {
645 if (isset($parms[$paramname])) {
646 unset($parms[$paramname]);
651 } else {
652 // If no key and secret, do the launch unsigned.
653 $returnurlparams['unsigned'] = '1';
654 $parms = $requestparams;
657 return array($endpoint, $parms);
661 * Launch an external tool activity.
663 * @param stdClass $instance the external tool activity settings
664 * @param int $foruserid for user param, optional
665 * @return string The HTML code containing the javascript code for the launch
667 function lti_launch_tool($instance, $foruserid=0) {
669 list($endpoint, $parms) = lti_get_launch_data($instance, '', '', $foruserid);
670 $debuglaunch = ( $instance->debuglaunch == 1 );
672 $content = lti_post_launch_html($parms, $endpoint, $debuglaunch);
674 echo $content;
678 * Prepares an LTI registration request message
680 * @param object $toolproxy Tool Proxy instance object
682 function lti_register($toolproxy) {
683 $endpoint = $toolproxy->regurl;
685 // Change the status to pending.
686 $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING;
687 lti_update_tool_proxy($toolproxy);
689 $requestparams = lti_build_registration_request($toolproxy);
691 $content = lti_post_launch_html($requestparams, $endpoint, false);
693 echo $content;
698 * Gets the parameters for the regirstration request
700 * @param object $toolproxy Tool Proxy instance object
701 * @return array Registration request parameters
703 function lti_build_registration_request($toolproxy) {
704 $key = $toolproxy->guid;
705 $secret = $toolproxy->secret;
707 $requestparams = array();
708 $requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest';
709 $requestparams['lti_version'] = 'LTI-2p0';
710 $requestparams['reg_key'] = $key;
711 $requestparams['reg_password'] = $secret;
712 $requestparams['reg_url'] = $toolproxy->regurl;
714 // Add the profile URL.
715 $profileservice = lti_get_service_by_name('profile');
716 $profileservice->set_tool_proxy($toolproxy);
717 $requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url');
719 // Add the return URL.
720 $returnurlparams = array('id' => $toolproxy->id, 'sesskey' => sesskey());
721 $url = new \moodle_url('/mod/lti/externalregistrationreturn.php', $returnurlparams);
722 $returnurl = $url->out(false);
724 $requestparams['launch_presentation_return_url'] = $returnurl;
726 return $requestparams;
730 /** get Organization ID using default if no value provided
731 * @param object $typeconfig
732 * @return string
734 function lti_get_organizationid($typeconfig) {
735 global $CFG;
736 // Default the organizationid if not specified.
737 if (empty($typeconfig['organizationid'])) {
738 if (($typeconfig['organizationid_default'] ?? LTI_DEFAULT_ORGID_SITEHOST) == LTI_DEFAULT_ORGID_SITEHOST) {
739 $urlparts = parse_url($CFG->wwwroot);
740 return $urlparts['host'];
741 } else {
742 return md5(get_site_identifier());
745 return $typeconfig['organizationid'];
749 * Build source ID
751 * @param int $instanceid
752 * @param int $userid
753 * @param string $servicesalt
754 * @param null|int $typeid
755 * @param null|int $launchid
756 * @return stdClass
758 function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) {
759 $data = new \stdClass();
761 $data->instanceid = $instanceid;
762 $data->userid = $userid;
763 $data->typeid = $typeid;
764 if (!empty($launchid)) {
765 $data->launchid = $launchid;
766 } else {
767 $data->launchid = mt_rand();
770 $json = json_encode($data);
772 $hash = hash('sha256', $json . $servicesalt, false);
774 $container = new \stdClass();
775 $container->data = $data;
776 $container->hash = $hash;
778 return $container;
782 * This function builds the request that must be sent to the tool producer
784 * @param object $instance Basic LTI instance object
785 * @param array $typeconfig Basic LTI tool configuration
786 * @param object $course Course object
787 * @param int|null $typeid Basic LTI tool ID
788 * @param boolean $islti2 True if an LTI 2 tool is being launched
789 * @param string $messagetype LTI Message Type for this launch
790 * @param int $foruserid User targeted by this launch
792 * @return array Request details
794 function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false,
795 $messagetype = 'basic-lti-launch-request', $foruserid = 0) {
796 global $USER, $CFG;
798 if (empty($instance->cmid)) {
799 $instance->cmid = 0;
802 $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2);
804 $requestparams = array(
805 'user_id' => $USER->id,
806 'lis_person_sourcedid' => $USER->idnumber,
807 'roles' => $role,
808 'context_id' => $course->id,
809 'context_label' => trim(html_to_text($course->shortname, 0)),
810 'context_title' => trim(html_to_text($course->fullname, 0)),
812 if ($foruserid) {
813 $requestparams['for_user_id'] = $foruserid;
815 if ($messagetype) {
816 $requestparams['lti_message_type'] = $messagetype;
818 if (!empty($instance->name)) {
819 $requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0));
821 if (!empty($instance->cmid)) {
822 $intro = format_module_intro('lti', $instance, $instance->cmid);
823 $intro = trim(html_to_text($intro, 0, false));
825 // This may look weird, but this is required for new lines
826 // so we generate the same OAuth signature as the tool provider.
827 $intro = str_replace("\n", "\r\n", $intro);
828 $requestparams['resource_link_description'] = $intro;
830 if (!empty($instance->id)) {
831 $requestparams['resource_link_id'] = $instance->id;
833 if (!empty($instance->resource_link_id)) {
834 $requestparams['resource_link_id'] = $instance->resource_link_id;
836 if ($course->format == 'site') {
837 $requestparams['context_type'] = 'Group';
838 } else {
839 $requestparams['context_type'] = 'CourseSection';
840 $requestparams['lis_course_section_sourcedid'] = $course->idnumber;
843 if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 ||
844 $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS ||
845 ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))
847 $placementsecret = $instance->servicesalt;
848 $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid));
849 $requestparams['lis_result_sourcedid'] = $sourcedid;
851 // Add outcome service URL.
852 $serviceurl = new \moodle_url('/mod/lti/service.php');
853 $serviceurl = $serviceurl->out();
855 $forcessl = false;
856 if (!empty($CFG->mod_lti_forcessl)) {
857 $forcessl = true;
860 if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) {
861 $serviceurl = lti_ensure_url_is_https($serviceurl);
864 $requestparams['lis_outcome_service_url'] = $serviceurl;
867 // Send user's name and email data if appropriate.
868 if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS ||
869 ($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname)
870 && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS)
872 $requestparams['lis_person_name_given'] = $USER->firstname;
873 $requestparams['lis_person_name_family'] = $USER->lastname;
874 $requestparams['lis_person_name_full'] = fullname($USER);
875 $requestparams['ext_user_username'] = $USER->username;
878 if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS ||
879 ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr)
880 && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS)
882 $requestparams['lis_person_contact_email_primary'] = $USER->email;
885 return $requestparams;
889 * This function builds the request that must be sent to an LTI 2 tool provider
891 * @param object $tool Basic LTI tool object
892 * @param array $params Custom launch parameters
894 * @return array Request details
896 function lti_build_request_lti2($tool, $params) {
898 $requestparams = array();
900 $capabilities = lti_get_capabilities();
901 $enabledcapabilities = explode("\n", $tool->enabledcapability);
902 foreach ($enabledcapabilities as $capability) {
903 if (array_key_exists($capability, $capabilities)) {
904 $val = $capabilities[$capability];
905 if ($val && (substr($val, 0, 1) != '$')) {
906 if (isset($params[$val])) {
907 $requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]];
913 return $requestparams;
918 * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer
920 * @param stdClass $instance Basic LTI instance object
921 * @param string $orgid Organisation ID
922 * @param boolean $islti2 True if an LTI 2 tool is being launched
923 * @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty.
925 * @return array Request details
926 * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
927 * @see lti_build_standard_message()
929 function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') {
930 if (!$islti2) {
931 $ltiversion = LTI_VERSION_1;
932 } else {
933 $ltiversion = LTI_VERSION_2;
935 return lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype);
939 * This function builds the standard parameters for an LTI message that must be sent to the tool producer
941 * @param stdClass $instance Basic LTI instance object
942 * @param string $orgid Organisation ID
943 * @param boolean $ltiversion LTI version to be used for tool messages
944 * @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty.
946 * @return array Message parameters
948 function lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype = 'basic-lti-launch-request') {
949 global $CFG;
951 $requestparams = array();
953 if ($instance) {
954 $requestparams['resource_link_id'] = $instance->id;
955 if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) {
956 $requestparams['resource_link_id'] = $instance->resource_link_id;
960 $requestparams['launch_presentation_locale'] = current_language();
962 // Make sure we let the tool know what LMS they are being called from.
963 $requestparams['ext_lms'] = 'moodle-2';
964 $requestparams['tool_consumer_info_product_family_code'] = 'moodle';
965 $requestparams['tool_consumer_info_version'] = strval($CFG->version);
967 // Add oauth_callback to be compliant with the 1.0A spec.
968 $requestparams['oauth_callback'] = 'about:blank';
970 $requestparams['lti_version'] = $ltiversion;
971 $requestparams['lti_message_type'] = $messagetype;
973 if ($orgid) {
974 $requestparams["tool_consumer_instance_guid"] = $orgid;
976 if (!empty($CFG->mod_lti_institution_name)) {
977 $requestparams['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0));
978 } else {
979 $requestparams['tool_consumer_instance_name'] = get_site()->shortname;
981 $requestparams['tool_consumer_instance_description'] = trim(html_to_text(get_site()->fullname, 0));
983 return $requestparams;
987 * This function builds the custom parameters
989 * @param object $toolproxy Tool proxy instance object
990 * @param object $tool Tool instance object
991 * @param object $instance Tool placement instance object
992 * @param array $params LTI launch parameters
993 * @param string $customstr Custom parameters defined for tool
994 * @param string $instructorcustomstr Custom parameters defined for this placement
995 * @param boolean $islti2 True if an LTI 2 tool is being launched
997 * @return array Custom parameters
999 function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) {
1001 // Concatenate the custom parameters from the administrator and the instructor
1002 // Instructor parameters are only taken into consideration if the administrator
1003 // has given permission.
1004 $custom = array();
1005 if ($customstr) {
1006 $custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2);
1008 if ($instructorcustomstr) {
1009 $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
1010 $instructorcustomstr, $islti2), $custom);
1012 if ($islti2) {
1013 $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
1014 $tool->parameter, true), $custom);
1015 $settings = lti_get_tool_settings($tool->toolproxyid);
1016 $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1017 if (!empty($instance->course)) {
1018 $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course);
1019 $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1020 if (!empty($instance->id)) {
1021 $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id);
1022 $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1027 return $custom;
1031 * Builds a standard LTI Content-Item selection request.
1033 * @param int $id The tool type ID.
1034 * @param stdClass $course The course object.
1035 * @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP)
1036 * will use to return the Content-Item message.
1037 * @param string $title The tool's title, if available.
1038 * @param string $text The text to display to represent the content item. This value may be a long description of the content item.
1039 * @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default.
1040 * @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened
1041 * (via the presentationDocumentTarget element for a returned content item).
1042 * If empty, "frame", "iframe", and "window" will be supported by default.
1043 * @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without
1044 * @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default.
1045 * any option for the user to cancel the operation. False by default.
1046 * @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not.
1047 * A signed message should always be required when the content item is being created automatically in the
1048 * TC without further interaction from the user. False by default.
1049 * @param bool $canconfirm Flag for can_confirm parameter. False by default.
1050 * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default.
1051 * @param string $nonce
1052 * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface.
1053 * @throws moodle_exception When the LTI tool type does not exist.`
1054 * @throws coding_exception For invalid media type and presentation target parameters.
1056 function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
1057 $presentationtargets = [], $autocreate = false, $multiple = true,
1058 $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {
1059 global $USER;
1061 $tool = lti_get_type($id);
1062 // Validate parameters.
1063 if (!$tool) {
1064 throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1066 if (!is_array($mediatypes)) {
1067 throw new coding_exception('The list of accepted media types should be in an array');
1069 if (!is_array($presentationtargets)) {
1070 throw new coding_exception('The list of accepted presentation targets should be in an array');
1073 // Check title. If empty, use the tool's name.
1074 if (empty($title)) {
1075 $title = $tool->name;
1078 $typeconfig = lti_get_type_config($id);
1079 $key = '';
1080 $secret = '';
1081 $islti2 = false;
1082 $islti13 = false;
1083 if (isset($tool->toolproxyid)) {
1084 $islti2 = true;
1085 $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
1086 $key = $toolproxy->guid;
1087 $secret = $toolproxy->secret;
1088 } else {
1089 $islti13 = $tool->ltiversion === LTI_VERSION_1P3;
1090 $toolproxy = null;
1091 if ($islti13 && !empty($tool->clientid)) {
1092 $key = $tool->clientid;
1093 } else if (!$islti13 && !empty($typeconfig['resourcekey'])) {
1094 $key = $typeconfig['resourcekey'];
1096 if (!empty($typeconfig['password'])) {
1097 $secret = $typeconfig['password'];
1100 $tool->enabledcapability = '';
1101 if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) {
1102 $tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest'];
1105 $tool->parameter = '';
1106 if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) {
1107 $tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest'];
1110 // Set the tool URL.
1111 if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) {
1112 $toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']);
1113 } else {
1114 $toolurl = new moodle_url($typeconfig['toolurl']);
1117 // Check if SSL is forced.
1118 if (!empty($typeconfig['forcessl'])) {
1119 // Make sure the tool URL is set to https.
1120 if (strtolower($toolurl->get_scheme()) === 'http') {
1121 $toolurl->set_scheme('https');
1123 // Make sure the return URL is set to https.
1124 if (strtolower($returnurl->get_scheme()) === 'http') {
1125 $returnurl->set_scheme('https');
1128 $toolurlout = $toolurl->out(false);
1130 // Get base request parameters.
1131 $instance = new stdClass();
1132 $instance->course = $course->id;
1133 $requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2);
1135 // Get LTI2-specific request parameters and merge to the request parameters if applicable.
1136 if ($islti2) {
1137 $lti2params = lti_build_request_lti2($tool, $requestparams);
1138 $requestparams = array_merge($requestparams, $lti2params);
1141 // Get standard request parameters and merge to the request parameters.
1142 $orgid = lti_get_organizationid($typeconfig);
1143 $standardparams = lti_build_standard_message(null, $orgid, $tool->ltiversion, 'ContentItemSelectionRequest');
1144 $requestparams = array_merge($requestparams, $standardparams);
1146 // Get custom request parameters and merge to the request parameters.
1147 $customstr = '';
1148 if (!empty($typeconfig['customparameters'])) {
1149 $customstr = $typeconfig['customparameters'];
1151 $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2);
1152 $requestparams = array_merge($requestparams, $customparams);
1154 // Add the parameters configured by the LTI services.
1155 if ($id && !$islti2) {
1156 $services = lti_get_services();
1157 foreach ($services as $service) {
1158 $serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
1159 $course->id, $USER->id , $id);
1160 foreach ($serviceparameters as $paramkey => $paramvalue) {
1161 $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
1162 $islti2);
1167 // Allow request params to be updated by sub-plugins.
1168 $plugins = core_component::get_plugin_list('ltisource');
1169 foreach (array_keys($plugins) as $plugin) {
1170 $pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []);
1172 if (!empty($pluginparams) && is_array($pluginparams)) {
1173 $requestparams = array_merge($requestparams, $pluginparams);
1177 if (!$islti13) {
1178 // Media types. Set to ltilink by default if empty.
1179 if (empty($mediatypes)) {
1180 $mediatypes = [
1181 'application/vnd.ims.lti.v1.ltilink',
1184 $requestparams['accept_media_types'] = implode(',', $mediatypes);
1185 } else {
1186 // Only LTI links are currently supported.
1187 $requestparams['accept_types'] = 'ltiResourceLink';
1190 // Presentation targets. Supports frame, iframe, window by default if empty.
1191 if (empty($presentationtargets)) {
1192 $presentationtargets = [
1193 'frame',
1194 'iframe',
1195 'window',
1198 $requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets);
1200 // Other request parameters.
1201 $requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false';
1202 $requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false';
1203 $requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false';
1204 $requestparams['auto_create'] = $autocreate === true ? 'true' : 'false';
1205 $requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false';
1206 $requestparams['content_item_return_url'] = $returnurl->out(false);
1207 $requestparams['title'] = $title;
1208 $requestparams['text'] = $text;
1209 if (!$islti13) {
1210 $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret);
1211 } else {
1212 $signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce);
1214 $toolurlparams = $toolurl->params();
1216 // Strip querystring params in endpoint url from $signedparams to avoid duplication.
1217 if (!empty($toolurlparams) && !empty($signedparams)) {
1218 foreach (array_keys($toolurlparams) as $paramname) {
1219 if (isset($signedparams[$paramname])) {
1220 unset($signedparams[$paramname]);
1225 // Check for params that should not be passed. Unset if they are set.
1226 $unwantedparams = [
1227 'resource_link_id',
1228 'resource_link_title',
1229 'resource_link_description',
1230 'launch_presentation_return_url',
1231 'lis_result_sourcedid',
1233 foreach ($unwantedparams as $param) {
1234 if (isset($signedparams[$param])) {
1235 unset($signedparams[$param]);
1239 // Prepare result object.
1240 $result = new stdClass();
1241 $result->params = $signedparams;
1242 $result->url = $toolurlout;
1244 return $result;
1248 * Verifies the OAuth signature of an incoming message.
1250 * @param int $typeid The tool type ID.
1251 * @param string $consumerkey The consumer key.
1252 * @return stdClass Tool type
1253 * @throws moodle_exception
1254 * @throws lti\OAuthException
1256 function lti_verify_oauth_signature($typeid, $consumerkey) {
1257 $tool = lti_get_type($typeid);
1258 // Validate parameters.
1259 if (!$tool) {
1260 throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1262 $typeconfig = lti_get_type_config($typeid);
1264 if (isset($tool->toolproxyid)) {
1265 $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
1266 $key = $toolproxy->guid;
1267 $secret = $toolproxy->secret;
1268 } else {
1269 $toolproxy = null;
1270 if (!empty($typeconfig['resourcekey'])) {
1271 $key = $typeconfig['resourcekey'];
1272 } else {
1273 $key = '';
1275 if (!empty($typeconfig['password'])) {
1276 $secret = $typeconfig['password'];
1277 } else {
1278 $secret = '';
1282 if ($consumerkey !== $key) {
1283 throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1286 $store = new lti\TrivialOAuthDataStore();
1287 $store->add_consumer($key, $secret);
1288 $server = new lti\OAuthServer($store);
1289 $method = new lti\OAuthSignatureMethod_HMAC_SHA1();
1290 $server->add_signature_method($method);
1291 $request = lti\OAuthRequest::from_request();
1292 try {
1293 $server->verify_request($request);
1294 } catch (lti\OAuthException $e) {
1295 throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage());
1298 return $tool;
1302 * Verifies the JWT signature using a JWK keyset.
1304 * @param string $jwtparam JWT parameter value.
1305 * @param string $keyseturl The tool keyseturl.
1306 * @param string $clientid The tool client id.
1308 * @return object The JWT's payload as a PHP object
1309 * @throws moodle_exception
1310 * @throws UnexpectedValueException Provided JWT was invalid
1311 * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
1312 * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1313 * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
1314 * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
1316 function lti_verify_with_keyset($jwtparam, $keyseturl, $clientid) {
1317 // Attempts to retrieve cached keyset.
1318 $cache = cache::make('mod_lti', 'keyset');
1319 $keyset = $cache->get($clientid);
1321 try {
1322 if (empty($keyset)) {
1323 throw new moodle_exception('errornocachedkeysetfound', 'mod_lti');
1325 $keysetarr = json_decode($keyset, true);
1326 // JWK::parseKeySet uses RS256 algorithm by default.
1327 $keys = JWK::parseKeySet($keysetarr);
1328 $jwt = JWT::decode($jwtparam, $keys);
1329 } catch (Exception $e) {
1330 // Something went wrong, so attempt to update cached keyset and then try again.
1331 $keyset = download_file_content($keyseturl);
1332 $keysetarr = json_decode($keyset, true);
1334 // Fix for firebase/php-jwt's dependency on the optional 'alg' property in the JWK.
1335 $keysetarr = jwks_helper::fix_jwks_alg($keysetarr, $jwtparam);
1337 // JWK::parseKeySet uses RS256 algorithm by default.
1338 $keys = JWK::parseKeySet($keysetarr);
1339 $jwt = JWT::decode($jwtparam, $keys);
1340 // If sucessful, updates the cached keyset.
1341 $cache->set($clientid, $keyset);
1343 return $jwt;
1347 * Verifies the JWT signature of an incoming message.
1349 * @param int $typeid The tool type ID.
1350 * @param string $consumerkey The consumer key.
1351 * @param string $jwtparam JWT parameter value
1353 * @return stdClass Tool type
1354 * @throws moodle_exception
1355 * @throws UnexpectedValueException Provided JWT was invalid
1356 * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
1357 * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1358 * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
1359 * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
1361 function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
1362 $tool = lti_get_type($typeid);
1364 // Validate parameters.
1365 if (!$tool) {
1366 throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1368 if (isset($tool->toolproxyid)) {
1369 throw new moodle_exception('JWT security not supported with LTI 2');
1372 $typeconfig = lti_get_type_config($typeid);
1374 $key = $tool->clientid ?? '';
1376 if ($consumerkey !== $key) {
1377 throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1380 if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) {
1381 $publickey = $typeconfig['publickey'] ?? '';
1382 if (empty($publickey)) {
1383 throw new moodle_exception('No public key configured');
1385 // Attemps to verify jwt with RSA key.
1386 JWT::decode($jwtparam, new Key($publickey, 'RS256'));
1387 } else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) {
1388 $keyseturl = $typeconfig['publickeyset'] ?? '';
1389 if (empty($keyseturl)) {
1390 throw new moodle_exception('No public keyset configured');
1392 // Attempts to verify jwt with jwk keyset.
1393 lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid);
1394 } else {
1395 throw new moodle_exception('Invalid public key type');
1398 return $tool;
1402 * Converts an array of custom parameters to a new line separated string.
1404 * @param object $params list of params to concatenate
1406 * @return string
1408 function params_to_string(object $params) {
1409 $customparameters = [];
1410 foreach ($params as $key => $value) {
1411 $customparameters[] = "{$key}={$value}";
1413 return implode("\n", $customparameters);
1417 * Converts LTI 1.1 Content Item for LTI Link to Form data.
1419 * @param object $tool Tool for which the item is created for.
1420 * @param object $typeconfig The tool configuration.
1421 * @param object $item Item populated from JSON to be converted to Form form
1423 * @return stdClass Form config for the item
1425 function content_item_to_form(object $tool, object $typeconfig, object $item) : stdClass {
1426 $config = new stdClass();
1427 $config->name = '';
1428 if (isset($item->title)) {
1429 $config->name = $item->title;
1431 if (empty($config->name)) {
1432 $config->name = $tool->name;
1434 if (isset($item->text)) {
1435 $config->introeditor = [
1436 'text' => $item->text,
1437 'format' => FORMAT_PLAIN
1439 } else {
1440 $config->introeditor = [
1441 'text' => '',
1442 'format' => FORMAT_PLAIN
1445 if (isset($item->icon->{'@id'})) {
1446 $iconurl = new moodle_url($item->icon->{'@id'});
1447 // Assign item's icon URL to secureicon or icon depending on its scheme.
1448 if (strtolower($iconurl->get_scheme()) === 'https') {
1449 $config->secureicon = $iconurl->out(false);
1450 } else {
1451 $config->icon = $iconurl->out(false);
1454 if (isset($item->url)) {
1455 $url = new moodle_url($item->url);
1456 $config->toolurl = $url->out(false);
1457 $config->typeid = 0;
1458 } else {
1459 $config->typeid = $tool->id;
1461 $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
1462 $islti2 = $tool->ltiversion === LTI_VERSION_2;
1463 if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
1464 $acceptgrades = $typeconfig->lti_acceptgrades;
1465 if ($acceptgrades == LTI_SETTING_ALWAYS) {
1466 // We create a line item regardless if the definition contains one or not.
1467 $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1468 $config->grade_modgrade_point = 100;
1470 if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
1471 if (isset($item->lineItem)) {
1472 $lineitem = $item->lineItem;
1473 $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1474 $maxscore = 100;
1475 if (isset($lineitem->scoreConstraints)) {
1476 $sc = $lineitem->scoreConstraints;
1477 if (isset($sc->totalMaximum)) {
1478 $maxscore = $sc->totalMaximum;
1479 } else if (isset($sc->normalMaximum)) {
1480 $maxscore = $sc->normalMaximum;
1483 $config->grade_modgrade_point = $maxscore;
1484 $config->lineitemresourceid = '';
1485 $config->lineitemtag = '';
1486 $config->lineitemsubreviewurl = '';
1487 $config->lineitemsubreviewparams = '';
1488 if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
1489 $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';
1491 if (isset($lineitem->tag)) {
1492 $config->lineitemtag = $lineitem->tag?:'';
1494 if (isset($lineitem->submissionReview)) {
1495 $subreview = $lineitem->submissionReview;
1496 $config->lineitemsubreviewurl = 'DEFAULT';
1497 if (!empty($subreview->url)) {
1498 $config->lineitemsubreviewurl = $subreview->url;
1500 if (isset($subreview->custom)) {
1501 $config->lineitemsubreviewparams = params_to_string($subreview->custom);
1507 $config->instructorchoicesendname = LTI_SETTING_NEVER;
1508 $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
1509 $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
1510 if (isset($item->placementAdvice->presentationDocumentTarget)) {
1511 if ($item->placementAdvice->presentationDocumentTarget === 'window') {
1512 $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW;
1513 } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') {
1514 $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
1515 } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') {
1516 $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED;
1519 if (isset($item->custom)) {
1520 $config->instructorcustomparameters = params_to_string($item->custom);
1522 return $config;
1526 * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
1527 * selected content item. This configuration data can be then used when adding a tool into the course.
1529 * @param int $typeid The tool type ID.
1530 * @param string $messagetype The value for the lti_message_type parameter.
1531 * @param string $ltiversion The value for the lti_version parameter.
1532 * @param string $consumerkey The consumer key.
1533 * @param string $contentitemsjson The JSON string for the content_items parameter.
1534 * @return stdClass The array of module information objects.
1535 * @throws moodle_exception
1536 * @throws lti\OAuthException
1538 function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
1539 $tool = lti_get_type($typeid);
1540 // Validate parameters.
1541 if (!$tool) {
1542 throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1544 // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
1545 // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
1546 if ($messagetype !== 'ContentItemSelection') {
1547 debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
1548 DEBUG_DEVELOPER);
1551 // Check LTI versions from our side and the response's side. Show debugging if they don't match.
1552 // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
1553 $expectedversion = $tool->ltiversion;
1554 $islti2 = ($expectedversion === LTI_VERSION_2);
1555 if ($ltiversion !== $expectedversion) {
1556 debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
1557 " Response: {$ltiversion}", DEBUG_DEVELOPER);
1560 $items = json_decode($contentitemsjson);
1561 if (empty($items)) {
1562 throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
1564 if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {
1565 throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
1568 $config = null;
1569 $items = $items->{'@graph'};
1570 if (!empty($items)) {
1571 $typeconfig = lti_get_type_type_config($tool->id);
1572 if (count($items) == 1) {
1573 $config = content_item_to_form($tool, $typeconfig, $items[0]);
1574 } else {
1575 $multiple = [];
1576 foreach ($items as $item) {
1577 $multiple[] = content_item_to_form($tool, $typeconfig, $item);
1579 $config = new stdClass();
1580 $config->multiple = $multiple;
1583 return $config;
1587 * Converts the new Deep-Linking format for Content-Items to the old format.
1589 * @param string $param JSON string representing new Deep-Linking format
1590 * @return string JSON representation of content-items
1592 function lti_convert_content_items($param) {
1593 $items = array();
1594 $json = json_decode($param);
1595 if (!empty($json) && is_array($json)) {
1596 foreach ($json as $item) {
1597 if (isset($item->type)) {
1598 $newitem = clone $item;
1599 switch ($item->type) {
1600 case 'ltiResourceLink':
1601 $newitem->{'@type'} = 'LtiLinkItem';
1602 $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1603 break;
1604 case 'link':
1605 case 'rich':
1606 $newitem->{'@type'} = 'ContentItem';
1607 $newitem->mediaType = 'text/html';
1608 break;
1609 case 'file':
1610 $newitem->{'@type'} = 'FileItem';
1611 break;
1613 unset($newitem->type);
1614 if (isset($item->html)) {
1615 $newitem->text = $item->html;
1616 unset($newitem->html);
1618 if (isset($item->iframe)) {
1619 // DeepLinking allows multiple options to be declared as supported.
1620 // We favor iframe over new window if both are specified.
1621 $newitem->placementAdvice = new stdClass();
1622 $newitem->placementAdvice->presentationDocumentTarget = 'iframe';
1623 if (isset($item->iframe->width)) {
1624 $newitem->placementAdvice->displayWidth = $item->iframe->width;
1626 if (isset($item->iframe->height)) {
1627 $newitem->placementAdvice->displayHeight = $item->iframe->height;
1629 unset($newitem->iframe);
1630 unset($newitem->window);
1631 } else if (isset($item->window)) {
1632 $newitem->placementAdvice = new stdClass();
1633 $newitem->placementAdvice->presentationDocumentTarget = 'window';
1634 if (isset($item->window->targetName)) {
1635 $newitem->placementAdvice->windowTarget = $item->window->targetName;
1637 if (isset($item->window->width)) {
1638 $newitem->placementAdvice->displayWidth = $item->window->width;
1640 if (isset($item->window->height)) {
1641 $newitem->placementAdvice->displayHeight = $item->window->height;
1643 unset($newitem->window);
1644 } else if (isset($item->presentation)) {
1645 // This may have been part of an early draft but is not in the final spec
1646 // so keeping it around for now in case it's actually been used.
1647 $newitem->placementAdvice = new stdClass();
1648 if (isset($item->presentation->documentTarget)) {
1649 $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
1651 if (isset($item->presentation->windowTarget)) {
1652 $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget;
1654 if (isset($item->presentation->width)) {
1655 $newitem->placementAdvice->dislayWidth = $item->presentation->width;
1657 if (isset($item->presentation->height)) {
1658 $newitem->placementAdvice->dislayHeight = $item->presentation->height;
1660 unset($newitem->presentation);
1662 if (isset($item->icon) && isset($item->icon->url)) {
1663 $newitem->icon->{'@id'} = $item->icon->url;
1664 unset($newitem->icon->url);
1666 if (isset($item->thumbnail) && isset($item->thumbnail->url)) {
1667 $newitem->thumbnail->{'@id'} = $item->thumbnail->url;
1668 unset($newitem->thumbnail->url);
1670 if (isset($item->lineItem)) {
1671 unset($newitem->lineItem);
1672 $newitem->lineItem = new stdClass();
1673 $newitem->lineItem->{'@type'} = 'LineItem';
1674 $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore';
1675 if (isset($item->lineItem->label)) {
1676 $newitem->lineItem->label = $item->lineItem->label;
1678 if (isset($item->lineItem->resourceId)) {
1679 $newitem->lineItem->assignedActivity = new stdClass();
1680 $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId;
1682 if (isset($item->lineItem->tag)) {
1683 $newitem->lineItem->tag = $item->lineItem->tag;
1685 if (isset($item->lineItem->scoreMaximum)) {
1686 $newitem->lineItem->scoreConstraints = new stdClass();
1687 $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits';
1688 $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum;
1690 if (isset($item->lineItem->submissionReview)) {
1691 $newitem->lineItem->submissionReview = $item->lineItem->submissionReview;
1694 $items[] = $newitem;
1699 $newitems = new stdClass();
1700 $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1701 $newitems->{'@graph'} = $items;
1703 return json_encode($newitems);
1706 function lti_get_tool_table($tools, $id) {
1707 global $OUTPUT;
1708 $html = '';
1710 $typename = get_string('typename', 'lti');
1711 $baseurl = get_string('baseurl', 'lti');
1712 $action = get_string('action', 'lti');
1713 $createdon = get_string('createdon', 'lti');
1715 if (!empty($tools)) {
1716 $html .= "
1717 <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\">
1718 <table id=\"{$id}_tools\">
1719 <thead>
1720 <tr>
1721 <th>$typename</th>
1722 <th>$baseurl</th>
1723 <th>$createdon</th>
1724 <th>$action</th>
1725 </tr>
1726 </thead>
1729 foreach ($tools as $type) {
1730 $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1731 $accept = get_string('accept', 'lti');
1732 $update = get_string('update', 'lti');
1733 $delete = get_string('delete', 'lti');
1735 if (empty($type->toolproxyid)) {
1736 $baseurl = new \moodle_url('/mod/lti/typessettings.php', array(
1737 'action' => 'accept',
1738 'id' => $type->id,
1739 'sesskey' => sesskey(),
1740 'tab' => $id
1742 $ref = $type->baseurl;
1743 } else {
1744 $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array(
1745 'action' => 'accept',
1746 'id' => $type->id,
1747 'sesskey' => sesskey(),
1748 'tab' => $id
1750 $ref = $type->tpname;
1753 $accepthtml = $OUTPUT->action_icon($baseurl,
1754 new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1755 array('title' => $accept, 'class' => 'editing_accept'));
1757 $deleteaction = 'delete';
1759 if ($type->state == LTI_TOOL_STATE_CONFIGURED) {
1760 $accepthtml = '';
1763 if ($type->state != LTI_TOOL_STATE_REJECTED) {
1764 $deleteaction = 'reject';
1765 $delete = get_string('reject', 'lti');
1768 $updateurl = clone($baseurl);
1769 $updateurl->param('action', 'update');
1770 $updatehtml = $OUTPUT->action_icon($updateurl,
1771 new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1772 array('title' => $update, 'class' => 'editing_update'));
1774 if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) {
1775 $deleteurl = clone($baseurl);
1776 $deleteurl->param('action', $deleteaction);
1777 $deletehtml = $OUTPUT->action_icon($deleteurl,
1778 new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1779 array('title' => $delete, 'class' => 'editing_delete'));
1780 } else {
1781 $deletehtml = '';
1783 $html .= "
1784 <tr>
1785 <td>
1786 {$type->name}
1787 </td>
1788 <td>
1789 {$ref}
1790 </td>
1791 <td>
1792 {$date}
1793 </td>
1794 <td align=\"center\">
1795 {$accepthtml}{$updatehtml}{$deletehtml}
1796 </td>
1797 </tr>
1800 $html .= '</table></div>';
1801 } else {
1802 $html .= get_string('no_' . $id, 'lti');
1805 return $html;
1809 * This function builds the tab for a category of tool proxies
1811 * @param object $toolproxies Tool proxy instance objects
1812 * @param string $id Category ID
1814 * @return string HTML for tab
1816 function lti_get_tool_proxy_table($toolproxies, $id) {
1817 global $OUTPUT;
1819 if (!empty($toolproxies)) {
1820 $typename = get_string('typename', 'lti');
1821 $url = get_string('registrationurl', 'lti');
1822 $action = get_string('action', 'lti');
1823 $createdon = get_string('createdon', 'lti');
1825 $html = <<< EOD
1826 <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em">
1827 <table id="{$id}_tool_proxies">
1828 <thead>
1829 <tr>
1830 <th>{$typename}</th>
1831 <th>{$url}</th>
1832 <th>{$createdon}</th>
1833 <th>{$action}</th>
1834 </tr>
1835 </thead>
1836 EOD;
1837 foreach ($toolproxies as $toolproxy) {
1838 $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1839 $accept = get_string('register', 'lti');
1840 $update = get_string('update', 'lti');
1841 $delete = get_string('delete', 'lti');
1843 $baseurl = new \moodle_url('/mod/lti/registersettings.php', array(
1844 'action' => 'accept',
1845 'id' => $toolproxy->id,
1846 'sesskey' => sesskey(),
1847 'tab' => $id
1850 $registerurl = new \moodle_url('/mod/lti/register.php', array(
1851 'id' => $toolproxy->id,
1852 'sesskey' => sesskey(),
1853 'tab' => 'tool_proxy'
1856 $accepthtml = $OUTPUT->action_icon($registerurl,
1857 new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1858 array('title' => $accept, 'class' => 'editing_accept'));
1860 $deleteaction = 'delete';
1862 if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) {
1863 $accepthtml = '';
1866 if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) {
1867 $delete = get_string('cancel', 'lti');
1870 $updateurl = clone($baseurl);
1871 $updateurl->param('action', 'update');
1872 $updatehtml = $OUTPUT->action_icon($updateurl,
1873 new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1874 array('title' => $update, 'class' => 'editing_update'));
1876 $deleteurl = clone($baseurl);
1877 $deleteurl->param('action', $deleteaction);
1878 $deletehtml = $OUTPUT->action_icon($deleteurl,
1879 new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1880 array('title' => $delete, 'class' => 'editing_delete'));
1881 $html .= <<< EOD
1882 <tr>
1883 <td>
1884 {$toolproxy->name}
1885 </td>
1886 <td>
1887 {$toolproxy->regurl}
1888 </td>
1889 <td>
1890 {$date}
1891 </td>
1892 <td align="center">
1893 {$accepthtml}{$updatehtml}{$deletehtml}
1894 </td>
1895 </tr>
1896 EOD;
1898 $html .= '</table></div>';
1899 } else {
1900 $html = get_string('no_' . $id, 'lti');
1903 return $html;
1907 * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter
1909 * @param object $tool Tool instance object
1911 * @return array List of enabled capabilities
1913 function lti_get_enabled_capabilities($tool) {
1914 if (!isset($tool)) {
1915 return array();
1917 if (!empty($tool->enabledcapability)) {
1918 $enabledcapabilities = explode("\n", $tool->enabledcapability);
1919 } else {
1920 $enabledcapabilities = array();
1922 if (!empty($tool->parameter)) {
1923 $paramstr = str_replace("\r\n", "\n", $tool->parameter);
1924 $paramstr = str_replace("\n\r", "\n", $paramstr);
1925 $paramstr = str_replace("\r", "\n", $paramstr);
1926 $params = explode("\n", $paramstr);
1927 foreach ($params as $param) {
1928 $pos = strpos($param, '=');
1929 if (($pos === false) || ($pos < 1)) {
1930 continue;
1932 $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
1933 if (substr($value, 0, 1) == '$') {
1934 $value = substr($value, 1);
1935 if (!in_array($value, $enabledcapabilities)) {
1936 $enabledcapabilities[] = $value;
1941 return $enabledcapabilities;
1945 * Splits the custom parameters
1947 * @param string $customstr String containing the parameters
1949 * @return array of custom parameters
1951 function lti_split_parameters($customstr) {
1952 $customstr = str_replace("\r\n", "\n", $customstr);
1953 $customstr = str_replace("\n\r", "\n", $customstr);
1954 $customstr = str_replace("\r", "\n", $customstr);
1955 $lines = explode("\n", $customstr); // Or should this split on "/[\n;]/"?
1956 $retval = array();
1957 foreach ($lines as $line) {
1958 $pos = strpos($line, '=');
1959 if ( $pos === false || $pos < 1 ) {
1960 continue;
1962 $key = trim(core_text::substr($line, 0, $pos));
1963 $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
1964 $retval[$key] = $val;
1966 return $retval;
1970 * Splits the custom parameters field to the various parameters
1972 * @param object $toolproxy Tool proxy instance object
1973 * @param object $tool Tool instance object
1974 * @param array $params LTI launch parameters
1975 * @param string $customstr String containing the parameters
1976 * @param boolean $islti2 True if an LTI 2 tool is being launched
1978 * @return array of custom parameters
1980 function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {
1981 $splitted = lti_split_parameters($customstr);
1982 $retval = array();
1983 foreach ($splitted as $key => $val) {
1984 $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
1985 $key2 = lti_map_keyname($key);
1986 $retval['custom_'.$key2] = $val;
1987 if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) {
1988 $retval['custom_'.$key] = $val;
1991 return $retval;
1995 * Adds the custom parameters to an array
1997 * @param object $toolproxy Tool proxy instance object
1998 * @param object $tool Tool instance object
1999 * @param array $params LTI launch parameters
2000 * @param array $parameters Array containing the parameters
2002 * @return array Array of custom parameters
2004 function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {
2005 $retval = array();
2006 foreach ($parameters as $key => $val) {
2007 $key2 = lti_map_keyname($key);
2008 $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true);
2009 $retval['custom_'.$key2] = $val;
2010 if ($key != $key2) {
2011 $retval['custom_'.$key] = $val;
2014 return $retval;
2018 * Parse a custom parameter to replace any substitution variables
2020 * @param object $toolproxy Tool proxy instance object
2021 * @param object $tool Tool instance object
2022 * @param array $params LTI launch parameters
2023 * @param string $value Custom parameter value
2024 * @param boolean $islti2 True if an LTI 2 tool is being launched
2026 * @return string Parsed value of custom parameter
2028 function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {
2029 // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var.
2030 global $USER, $COURSE;
2032 if ($value) {
2033 if (substr($value, 0, 1) == '\\') {
2034 $value = substr($value, 1);
2035 } else if (substr($value, 0, 1) == '$') {
2036 $value1 = substr($value, 1);
2037 $enabledcapabilities = lti_get_enabled_capabilities($tool);
2038 if (!$islti2 || in_array($value1, $enabledcapabilities)) {
2039 $capabilities = lti_get_capabilities();
2040 if (array_key_exists($value1, $capabilities)) {
2041 $val = $capabilities[$value1];
2042 if ($val) {
2043 if (substr($val, 0, 1) != '$') {
2044 $value = $params[$val];
2045 } else {
2046 $valarr = explode('->', substr($val, 1), 2);
2047 $value = "{${$valarr[0]}->{$valarr[1]}}";
2048 $value = str_replace('<br />' , ' ', $value);
2049 $value = str_replace('<br>' , ' ', $value);
2050 $value = format_string($value);
2052 } else {
2053 $value = lti_calculate_custom_parameter($value1);
2055 } else {
2056 $val = $value;
2057 $services = lti_get_services();
2058 foreach ($services as $service) {
2059 $service->set_tool_proxy($toolproxy);
2060 $service->set_type($tool);
2061 $value = $service->parse_value($val);
2062 if ($val != $value) {
2063 break;
2070 return $value;
2074 * Calculates the value of a custom parameter that has not been specified earlier
2076 * @param string $value Custom parameter value
2078 * @return string Calculated value of custom parameter
2080 function lti_calculate_custom_parameter($value) {
2081 global $USER, $COURSE;
2083 switch ($value) {
2084 case 'Moodle.Person.userGroupIds':
2085 return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]);
2086 case 'Context.id.history':
2087 return implode(",", get_course_history($COURSE));
2088 case 'CourseSection.timeFrame.begin':
2089 if (empty($COURSE->startdate)) {
2090 return "";
2092 $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC'));
2093 return $dt->format(DateTime::ATOM);
2094 case 'CourseSection.timeFrame.end':
2095 if (empty($COURSE->enddate)) {
2096 return "";
2098 $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC'));
2099 return $dt->format(DateTime::ATOM);
2101 return null;
2105 * Build the history chain for this course using the course originalcourseid.
2107 * @param object $course course for which the history is returned.
2109 * @return array ids of the source course in ancestry order, immediate parent 1st.
2111 function get_course_history($course) {
2112 global $DB;
2113 $history = [];
2114 $parentid = $course->originalcourseid;
2115 while (!empty($parentid) && !in_array($parentid, $history)) {
2116 $history[] = $parentid;
2117 $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid));
2119 return $history;
2123 * Used for building the names of the different custom parameters
2125 * @param string $key Parameter name
2126 * @param bool $tolower Do we want to convert the key into lower case?
2127 * @return string Processed name
2129 function lti_map_keyname($key, $tolower = true) {
2130 if ($tolower) {
2131 $newkey = '';
2132 $key = core_text::strtolower(trim($key));
2133 foreach (str_split($key) as $ch) {
2134 if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {
2135 $newkey .= $ch;
2136 } else {
2137 $newkey .= '_';
2140 } else {
2141 $newkey = $key;
2143 return $newkey;
2147 * Gets the IMS role string for the specified user and LTI course module.
2149 * @param mixed $user User object or user id
2150 * @param int $cmid The course module id of the LTI activity
2151 * @param int $courseid The course id of the LTI activity
2152 * @param boolean $islti2 True if an LTI 2 tool is being launched
2154 * @return string A role string suitable for passing with an LTI launch
2156 function lti_get_ims_role($user, $cmid, $courseid, $islti2) {
2157 $roles = array();
2159 if (empty($cmid)) {
2160 // If no cmid is passed, check if the user is a teacher in the course
2161 // This allows other modules to programmatically "fake" a launch without
2162 // a real LTI instance.
2163 $context = context_course::instance($courseid);
2165 if (has_capability('moodle/course:manageactivities', $context, $user)) {
2166 array_push($roles, 'Instructor');
2167 } else {
2168 array_push($roles, 'Learner');
2170 } else {
2171 $context = context_module::instance($cmid);
2173 if (has_capability('mod/lti:manage', $context)) {
2174 array_push($roles, 'Instructor');
2175 } else {
2176 array_push($roles, 'Learner');
2180 if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) {
2181 // Make sure admins do not have the Learner role, then set admin role.
2182 $roles = array_diff($roles, array('Learner'));
2183 if (!$islti2) {
2184 array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
2185 } else {
2186 array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator');
2190 return join(',', $roles);
2194 * Returns configuration details for the tool
2196 * @param int $typeid Basic LTI tool typeid
2198 * @return array Tool Configuration
2200 function lti_get_type_config($typeid) {
2201 global $DB;
2203 $query = "SELECT name, value
2204 FROM {lti_types_config}
2205 WHERE typeid = :typeid1
2206 UNION ALL
2207 SELECT 'toolurl' AS name, baseurl AS value
2208 FROM {lti_types}
2209 WHERE id = :typeid2
2210 UNION ALL
2211 SELECT 'icon' AS name, icon AS value
2212 FROM {lti_types}
2213 WHERE id = :typeid3
2214 UNION ALL
2215 SELECT 'secureicon' AS name, secureicon AS value
2216 FROM {lti_types}
2217 WHERE id = :typeid4";
2219 $typeconfig = array();
2220 $configs = $DB->get_records_sql($query,
2221 array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid));
2223 if (!empty($configs)) {
2224 foreach ($configs as $config) {
2225 $typeconfig[$config->name] = $config->value;
2229 return $typeconfig;
2232 function lti_get_tools_by_url($url, $state, $courseid = null) {
2233 $domain = lti_get_domain_from_url($url);
2235 return lti_get_tools_by_domain($domain, $state, $courseid);
2238 function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
2239 global $DB, $SITE;
2241 $statefilter = '';
2242 $coursefilter = '';
2244 if ($state) {
2245 $statefilter = 'AND state = :state';
2248 if ($courseid && $courseid != $SITE->id) {
2249 $coursefilter = 'OR course = :courseid';
2252 $query = "SELECT *
2253 FROM {lti_types}
2254 WHERE tooldomain = :tooldomain
2255 AND (course = :siteid $coursefilter)
2256 $statefilter";
2258 return $DB->get_records_sql($query, array(
2259 'courseid' => $courseid,
2260 'siteid' => $SITE->id,
2261 'tooldomain' => $domain,
2262 'state' => $state
2267 * Returns all basicLTI tools configured by the administrator
2269 * @param int $course
2271 * @return array
2273 function lti_filter_get_types($course) {
2274 global $DB;
2276 if (!empty($course)) {
2277 $where = "WHERE t.course = :course";
2278 $params = array('course' => $course);
2279 } else {
2280 $where = '';
2281 $params = array();
2283 $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname
2284 FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id
2285 {$where}";
2286 return $DB->get_records_sql($query, $params);
2290 * Given an array of tools, filter them based on their state
2292 * @param array $tools An array of lti_types records
2293 * @param int $state One of the LTI_TOOL_STATE_* constants
2294 * @return array
2296 function lti_filter_tool_types(array $tools, $state) {
2297 $return = array();
2298 foreach ($tools as $key => $tool) {
2299 if ($tool->state == $state) {
2300 $return[$key] = $tool;
2303 return $return;
2307 * Returns all lti types visible in this course
2309 * @param int $courseid The id of the course to retieve types for
2310 * @param array $coursevisible options for 'coursevisible' field,
2311 * default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]
2312 * @return stdClass[] All the lti types visible in the given course
2314 function lti_get_lti_types_by_course($courseid, $coursevisible = null) {
2315 global $DB, $SITE;
2317 if ($coursevisible === null) {
2318 $coursevisible = [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER];
2321 list($coursevisiblesql, $coursevisparams) = $DB->get_in_or_equal($coursevisible, SQL_PARAMS_NAMED, 'coursevisible');
2322 $courseconds = [];
2323 if (has_capability('mod/lti:addmanualinstance', context_course::instance($courseid))) {
2324 $courseconds[] = "course = :courseid";
2326 if (has_capability('mod/lti:addpreconfiguredinstance', context_course::instance($courseid))) {
2327 $courseconds[] = "course = :siteid";
2329 if (!$courseconds) {
2330 return [];
2332 $coursecond = implode(" OR ", $courseconds);
2333 $query = "SELECT *
2334 FROM {lti_types}
2335 WHERE coursevisible $coursevisiblesql
2336 AND ($coursecond)
2337 AND state = :active
2338 ORDER BY name ASC";
2340 return $DB->get_records_sql($query,
2341 array('siteid' => $SITE->id, 'courseid' => $courseid, 'active' => LTI_TOOL_STATE_CONFIGURED) + $coursevisparams);
2345 * Returns tool types for lti add instance and edit page
2347 * @return array Array of lti types
2349 function lti_get_types_for_add_instance() {
2350 global $COURSE;
2351 $admintypes = lti_get_lti_types_by_course($COURSE->id);
2353 $types = array();
2354 if (has_capability('mod/lti:addmanualinstance', context_course::instance($COURSE->id))) {
2355 $types[0] = (object)array('name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null);
2358 foreach ($admintypes as $type) {
2359 $types[$type->id] = $type;
2362 return $types;
2366 * Returns a list of configured types in the given course
2368 * @param int $courseid The id of the course to retieve types for
2369 * @param int $sectionreturn section to return to for forming the URLs
2370 * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link
2372 function lti_get_configured_types($courseid, $sectionreturn = 0) {
2373 global $OUTPUT;
2374 $types = array();
2375 $admintypes = lti_get_lti_types_by_course($courseid, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]);
2377 foreach ($admintypes as $ltitype) {
2378 $type = new stdClass();
2379 $type->id = $ltitype->id;
2380 $type->modclass = MOD_CLASS_ACTIVITY;
2381 $type->name = 'lti_type_' . $ltitype->id;
2382 // Clean the name. We don't want tags here.
2383 $type->title = clean_param($ltitype->name, PARAM_NOTAGS);
2384 $trimmeddescription = trim($ltitype->description ?? '');
2385 if ($trimmeddescription != '') {
2386 // Clean the description. We don't want tags here.
2387 $type->help = clean_param($trimmeddescription, PARAM_NOTAGS);
2388 $type->helplink = get_string('modulename_shortcut_link', 'lti');
2391 $iconurl = get_tool_type_icon_url($ltitype);
2392 $iconclass = '';
2393 if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) {
2394 // Do not filter the icon if it is not the default LTI activity icon.
2395 $iconclass = 'nofilter';
2397 $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]);
2399 $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'course' => $courseid,
2400 'sr' => $sectionreturn, 'typeid' => $ltitype->id));
2401 $types[] = $type;
2403 return $types;
2406 function lti_get_domain_from_url($url) {
2407 $matches = array();
2409 if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) {
2410 return $matches[1];
2414 function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) {
2415 $possibletools = lti_get_tools_by_url($url, $state, $courseid);
2417 return lti_get_best_tool_by_url($url, $possibletools, $courseid);
2420 function lti_get_url_thumbprint($url) {
2421 // Parse URL requires a schema otherwise everything goes into 'path'. Fixed 5.4.7 or later.
2422 if (preg_match('/https?:\/\//', $url) !== 1) {
2423 $url = 'http://'.$url;
2425 $urlparts = parse_url(strtolower($url));
2426 if (!isset($urlparts['path'])) {
2427 $urlparts['path'] = '';
2430 if (!isset($urlparts['query'])) {
2431 $urlparts['query'] = '';
2434 if (!isset($urlparts['host'])) {
2435 $urlparts['host'] = '';
2438 if (substr($urlparts['host'], 0, 4) === 'www.') {
2439 $urlparts['host'] = substr($urlparts['host'], 4);
2442 $urllower = $urlparts['host'] . '/' . $urlparts['path'];
2444 if ($urlparts['query'] != '') {
2445 $urllower .= '?' . $urlparts['query'];
2448 return $urllower;
2451 function lti_get_best_tool_by_url($url, $tools, $courseid = null) {
2452 if (count($tools) === 0) {
2453 return null;
2456 $urllower = lti_get_url_thumbprint($url);
2458 foreach ($tools as $tool) {
2459 $tool->_matchscore = 0;
2461 $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl);
2463 if ($urllower === $toolbaseurllower) {
2464 // 100 points for exact thumbprint match.
2465 $tool->_matchscore += 100;
2466 } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) {
2467 // 50 points if tool thumbprint starts with the base URL thumbprint.
2468 $tool->_matchscore += 50;
2471 // Prefer course tools over site tools.
2472 if (!empty($courseid)) {
2473 // Minus 10 points for not matching the course id (global tools).
2474 if ($tool->course != $courseid) {
2475 $tool->_matchscore -= 10;
2480 $bestmatch = array_reduce($tools, function($value, $tool) {
2481 if ($tool->_matchscore > $value->_matchscore) {
2482 return $tool;
2483 } else {
2484 return $value;
2487 }, (object)array('_matchscore' => -1));
2489 // None of the tools are suitable for this URL.
2490 if ($bestmatch->_matchscore <= 0) {
2491 return null;
2494 return $bestmatch;
2497 function lti_get_shared_secrets_by_key($key) {
2498 global $DB;
2500 // Look up the shared secret for the specified key in both the types_config table (for configured tools)
2501 // And in the lti resource table for ad-hoc tools.
2502 $lti13 = LTI_VERSION_1P3;
2503 $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value
2504 FROM {lti_types_config} t1
2505 JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
2506 JOIN {lti_types} type ON t2.typeid = type.id
2507 WHERE t1.name = 'resourcekey'
2508 AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1
2509 AND t2.name = 'password'
2510 AND type.state = :configured1
2511 AND type.ltiversion <> :ltiversion
2512 UNION
2513 SELECT tp.secret AS value
2514 FROM {lti_tool_proxies} tp
2515 JOIN {lti_types} t ON tp.id = t.toolproxyid
2516 WHERE tp.guid = :key2
2517 AND t.state = :configured2
2518 UNION
2519 SELECT password AS value
2520 FROM {lti}
2521 WHERE resourcekey = :key3";
2523 $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13,
2524 'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));
2526 $values = array_map(function($item) {
2527 return $item->value;
2528 }, $sharedsecrets);
2530 // There should really only be one shared secret per key. But, we can't prevent
2531 // more than one getting entered. For instance, if the same key is used for two tool providers.
2532 return $values;
2536 * Delete a Basic LTI configuration
2538 * @param int $id Configuration id
2540 function lti_delete_type($id) {
2541 global $DB;
2543 // We should probably just copy the launch URL to the tool instances in this case... using a single query.
2545 $instances = $DB->get_records('lti', array('typeid' => $id));
2546 foreach ($instances as $instance) {
2547 $instance->typeid = 0;
2548 $DB->update_record('lti', $instance);
2551 $DB->delete_records('lti_types', array('id' => $id));
2552 $DB->delete_records('lti_types_config', array('typeid' => $id));
2555 function lti_set_state_for_type($id, $state) {
2556 global $DB;
2558 $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));
2562 * Transforms a basic LTI object to an array
2564 * @param object $ltiobject Basic LTI object
2566 * @return array Basic LTI configuration details
2568 function lti_get_config($ltiobject) {
2569 $typeconfig = (array)$ltiobject;
2570 $additionalconfig = lti_get_type_config($ltiobject->typeid);
2571 $typeconfig = array_merge($typeconfig, $additionalconfig);
2572 return $typeconfig;
2577 * Generates some of the tool configuration based on the instance details
2579 * @param int $id
2581 * @return object configuration
2584 function lti_get_type_config_from_instance($id) {
2585 global $DB;
2587 $instance = $DB->get_record('lti', array('id' => $id));
2588 $config = lti_get_config($instance);
2590 $type = new \stdClass();
2591 $type->lti_fix = $id;
2592 if (isset($config['toolurl'])) {
2593 $type->lti_toolurl = $config['toolurl'];
2595 if (isset($config['instructorchoicesendname'])) {
2596 $type->lti_sendname = $config['instructorchoicesendname'];
2598 if (isset($config['instructorchoicesendemailaddr'])) {
2599 $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr'];
2601 if (isset($config['instructorchoiceacceptgrades'])) {
2602 $type->lti_acceptgrades = $config['instructorchoiceacceptgrades'];
2604 if (isset($config['instructorchoiceallowroster'])) {
2605 $type->lti_allowroster = $config['instructorchoiceallowroster'];
2608 if (isset($config['instructorcustomparameters'])) {
2609 $type->lti_allowsetting = $config['instructorcustomparameters'];
2611 return $type;
2615 * Generates some of the tool configuration based on the admin configuration details
2617 * @param int $id
2619 * @return stdClass Configuration details
2621 function lti_get_type_type_config($id) {
2622 global $DB;
2624 $basicltitype = $DB->get_record('lti_types', array('id' => $id));
2625 $config = lti_get_type_config($id);
2627 $type = new \stdClass();
2629 $type->lti_typename = $basicltitype->name;
2631 $type->typeid = $basicltitype->id;
2633 $type->toolproxyid = $basicltitype->toolproxyid;
2635 $type->lti_toolurl = $basicltitype->baseurl;
2637 $type->lti_ltiversion = $basicltitype->ltiversion;
2639 $type->lti_clientid = $basicltitype->clientid;
2640 $type->lti_clientid_disabled = $type->lti_clientid;
2642 $type->lti_description = $basicltitype->description;
2644 $type->lti_parameters = $basicltitype->parameter;
2646 $type->lti_icon = $basicltitype->icon;
2648 $type->lti_secureicon = $basicltitype->secureicon;
2650 if (isset($config['resourcekey'])) {
2651 $type->lti_resourcekey = $config['resourcekey'];
2653 if (isset($config['password'])) {
2654 $type->lti_password = $config['password'];
2656 if (isset($config['publickey'])) {
2657 $type->lti_publickey = $config['publickey'];
2659 if (isset($config['publickeyset'])) {
2660 $type->lti_publickeyset = $config['publickeyset'];
2662 if (isset($config['keytype'])) {
2663 $type->lti_keytype = $config['keytype'];
2665 if (isset($config['initiatelogin'])) {
2666 $type->lti_initiatelogin = $config['initiatelogin'];
2668 if (isset($config['redirectionuris'])) {
2669 $type->lti_redirectionuris = $config['redirectionuris'];
2672 if (isset($config['sendname'])) {
2673 $type->lti_sendname = $config['sendname'];
2675 if (isset($config['instructorchoicesendname'])) {
2676 $type->lti_instructorchoicesendname = $config['instructorchoicesendname'];
2678 if (isset($config['sendemailaddr'])) {
2679 $type->lti_sendemailaddr = $config['sendemailaddr'];
2681 if (isset($config['instructorchoicesendemailaddr'])) {
2682 $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr'];
2684 if (isset($config['acceptgrades'])) {
2685 $type->lti_acceptgrades = $config['acceptgrades'];
2687 if (isset($config['instructorchoiceacceptgrades'])) {
2688 $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades'];
2690 if (isset($config['allowroster'])) {
2691 $type->lti_allowroster = $config['allowroster'];
2693 if (isset($config['instructorchoiceallowroster'])) {
2694 $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster'];
2697 if (isset($config['customparameters'])) {
2698 $type->lti_customparameters = $config['customparameters'];
2701 if (isset($config['forcessl'])) {
2702 $type->lti_forcessl = $config['forcessl'];
2705 if (isset($config['organizationid_default'])) {
2706 $type->lti_organizationid_default = $config['organizationid_default'];
2707 } else {
2708 // Tool was configured before this option was available and the default then was host.
2709 $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
2711 if (isset($config['organizationid'])) {
2712 $type->lti_organizationid = $config['organizationid'];
2714 if (isset($config['organizationurl'])) {
2715 $type->lti_organizationurl = $config['organizationurl'];
2717 if (isset($config['organizationdescr'])) {
2718 $type->lti_organizationdescr = $config['organizationdescr'];
2720 if (isset($config['launchcontainer'])) {
2721 $type->lti_launchcontainer = $config['launchcontainer'];
2724 if (isset($config['coursevisible'])) {
2725 $type->lti_coursevisible = $config['coursevisible'];
2728 if (isset($config['contentitem'])) {
2729 $type->lti_contentitem = $config['contentitem'];
2732 if (isset($config['toolurl_ContentItemSelectionRequest'])) {
2733 $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];
2736 if (isset($config['debuglaunch'])) {
2737 $type->lti_debuglaunch = $config['debuglaunch'];
2740 if (isset($config['module_class_type'])) {
2741 $type->lti_module_class_type = $config['module_class_type'];
2744 // Get the parameters from the LTI services.
2745 foreach ($config as $name => $value) {
2746 if (strpos($name, 'ltiservice_') === 0) {
2747 $type->{$name} = $config[$name];
2751 return $type;
2754 function lti_prepare_type_for_save($type, $config) {
2755 if (isset($config->lti_toolurl)) {
2756 $type->baseurl = $config->lti_toolurl;
2757 if (isset($config->lti_tooldomain)) {
2758 $type->tooldomain = $config->lti_tooldomain;
2759 } else {
2760 $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
2763 if (isset($config->lti_description)) {
2764 $type->description = $config->lti_description;
2766 if (isset($config->lti_typename)) {
2767 $type->name = $config->lti_typename;
2769 if (isset($config->lti_ltiversion)) {
2770 $type->ltiversion = $config->lti_ltiversion;
2772 if (isset($config->lti_clientid)) {
2773 $type->clientid = $config->lti_clientid;
2775 if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {
2776 $type->clientid = registration_helper::get()->new_clientid();
2777 } else if (empty($type->clientid)) {
2778 $type->clientid = null;
2780 if (isset($config->lti_coursevisible)) {
2781 $type->coursevisible = $config->lti_coursevisible;
2784 if (isset($config->lti_icon)) {
2785 $type->icon = $config->lti_icon;
2787 if (isset($config->lti_secureicon)) {
2788 $type->secureicon = $config->lti_secureicon;
2791 $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0;
2792 $config->lti_forcessl = $type->forcessl;
2793 if (isset($config->lti_contentitem)) {
2794 $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;
2795 $config->lti_contentitem = $type->contentitem;
2797 if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
2798 if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {
2799 $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;
2800 } else {
2801 $type->toolurl_ContentItemSelectionRequest = '';
2803 $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;
2806 $type->timemodified = time();
2808 unset ($config->lti_typename);
2809 unset ($config->lti_toolurl);
2810 unset ($config->lti_description);
2811 unset ($config->lti_ltiversion);
2812 unset ($config->lti_clientid);
2813 unset ($config->lti_icon);
2814 unset ($config->lti_secureicon);
2817 function lti_update_type($type, $config) {
2818 global $DB, $CFG;
2820 lti_prepare_type_for_save($type, $config);
2822 if (lti_request_is_using_ssl() && !empty($type->secureicon)) {
2823 $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);
2824 } else {
2825 $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon));
2827 unset($config->oldicon);
2829 if ($DB->update_record('lti_types', $type)) {
2830 foreach ($config as $key => $value) {
2831 if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {
2832 $record = new \StdClass();
2833 $record->typeid = $type->id;
2834 $record->name = substr($key, 4);
2835 $record->value = $value;
2836 lti_update_config($record);
2838 if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
2839 $record = new \StdClass();
2840 $record->typeid = $type->id;
2841 $record->name = $key;
2842 $record->value = $value;
2843 lti_update_config($record);
2846 if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) {
2847 // We need to remove the tool proxy for this tool to function under 1.3.
2848 $toolproxyid = $type->toolproxyid;
2849 $DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid));
2850 $DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid));
2851 $type->toolproxyid = null;
2852 $DB->update_record('lti_types', $type);
2854 require_once($CFG->libdir.'/modinfolib.php');
2855 if ($clearcache) {
2856 $sql = "SELECT cm.id, cm.course
2857 FROM {course_modules} cm
2858 JOIN {modules} m ON cm.module = m.id
2859 JOIN {lti} l ON l.course = cm.course
2860 WHERE m.name = :name AND l.typeid = :typeid";
2862 $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]);
2864 $courseids = [];
2865 foreach ($rs as $record) {
2866 $courseids[] = $record->course;
2867 \course_modinfo::purge_course_module_cache($record->course, $record->id);
2869 $rs->close();
2870 $courseids = array_unique($courseids);
2871 foreach ($courseids as $courseid) {
2872 rebuild_course_cache($courseid, false, true);
2878 function lti_add_type($type, $config) {
2879 global $USER, $SITE, $DB;
2881 lti_prepare_type_for_save($type, $config);
2883 if (!isset($type->state)) {
2884 $type->state = LTI_TOOL_STATE_PENDING;
2887 if (!isset($type->ltiversion)) {
2888 $type->ltiversion = LTI_VERSION_1;
2891 if (!isset($type->timecreated)) {
2892 $type->timecreated = time();
2895 if (!isset($type->createdby)) {
2896 $type->createdby = $USER->id;
2899 if (!isset($type->course)) {
2900 $type->course = $SITE->id;
2903 // Create a salt value to be used for signing passed data to extension services
2904 // The outcome service uses the service salt on the instance. This can be used
2905 // for communication with services not related to a specific LTI instance.
2906 $config->lti_servicesalt = uniqid('', true);
2908 $id = $DB->insert_record('lti_types', $type);
2910 if ($id) {
2911 foreach ($config as $key => $value) {
2912 if (!is_null($value)) {
2913 if (substr($key, 0, 4) === 'lti_') {
2914 $fieldname = substr($key, 4);
2915 } else if (substr($key, 0, 11) !== 'ltiservice_') {
2916 continue;
2917 } else {
2918 $fieldname = $key;
2921 $record = new \StdClass();
2922 $record->typeid = $id;
2923 $record->name = $fieldname;
2924 $record->value = $value;
2926 lti_add_config($record);
2931 return $id;
2935 * Given an array of tool proxies, filter them based on their state
2937 * @param array $toolproxies An array of lti_tool_proxies records
2938 * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants
2940 * @return array
2942 function lti_filter_tool_proxy_types(array $toolproxies, $state) {
2943 $return = array();
2944 foreach ($toolproxies as $key => $toolproxy) {
2945 if ($toolproxy->state == $state) {
2946 $return[$key] = $toolproxy;
2949 return $return;
2953 * Get the tool proxy instance given its GUID
2955 * @param string $toolproxyguid Tool proxy GUID value
2957 * @return object
2959 function lti_get_tool_proxy_from_guid($toolproxyguid) {
2960 global $DB;
2962 $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid));
2964 return $toolproxy;
2968 * Get the tool proxy instance given its registration URL
2970 * @param string $regurl Tool proxy registration URL
2972 * @return array The record of the tool proxy with this url
2974 function lti_get_tool_proxies_from_registration_url($regurl) {
2975 global $DB;
2977 return $DB->get_records_sql(
2978 'SELECT * FROM {lti_tool_proxies}
2979 WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl',
2980 array('regurl' => $regurl)
2985 * Generates some of the tool proxy configuration based on the admin configuration details
2987 * @param int $id
2989 * @return mixed Tool Proxy details
2991 function lti_get_tool_proxy($id) {
2992 global $DB;
2994 $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id));
2995 return $toolproxy;
2999 * Returns lti tool proxies.
3001 * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them
3002 * @return array of basicLTI types
3004 function lti_get_tool_proxies($orphanedonly) {
3005 global $DB;
3007 if ($orphanedonly) {
3008 $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));
3009 $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3010 foreach ($proxies as $key => $value) {
3011 if (in_array($value->id, $usedproxyids)) {
3012 unset($proxies[$key]);
3015 return $proxies;
3016 } else {
3017 return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
3022 * Generates some of the tool proxy configuration based on the admin configuration details
3024 * @param int $id
3026 * @return mixed Tool Proxy details
3028 function lti_get_tool_proxy_config($id) {
3029 $toolproxy = lti_get_tool_proxy($id);
3031 $tp = new \stdClass();
3032 $tp->lti_registrationname = $toolproxy->name;
3033 $tp->toolproxyid = $toolproxy->id;
3034 $tp->state = $toolproxy->state;
3035 $tp->lti_registrationurl = $toolproxy->regurl;
3036 $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered);
3037 $tp->lti_services = explode("\n", $toolproxy->serviceoffered);
3039 return $tp;
3043 * Update the database with a tool proxy instance
3045 * @param object $config Tool proxy definition
3047 * @return int Record id number
3049 function lti_add_tool_proxy($config) {
3050 global $USER, $DB;
3052 $toolproxy = new \stdClass();
3053 if (isset($config->lti_registrationname)) {
3054 $toolproxy->name = trim($config->lti_registrationname);
3056 if (isset($config->lti_registrationurl)) {
3057 $toolproxy->regurl = trim($config->lti_registrationurl);
3059 if (isset($config->lti_capabilities)) {
3060 $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities);
3061 } else {
3062 $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities()));
3064 if (isset($config->lti_services)) {
3065 $toolproxy->serviceoffered = implode("\n", $config->lti_services);
3066 } else {
3067 $func = function($s) {
3068 return $s->get_id();
3070 $servicenames = array_map($func, lti_get_services());
3071 $toolproxy->serviceoffered = implode("\n", $servicenames);
3073 if (isset($config->toolproxyid) && !empty($config->toolproxyid)) {
3074 $toolproxy->id = $config->toolproxyid;
3075 if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) {
3076 $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3077 $toolproxy->guid = random_string();
3078 $toolproxy->secret = random_string();
3080 $id = lti_update_tool_proxy($toolproxy);
3081 } else {
3082 $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3083 $toolproxy->timemodified = time();
3084 $toolproxy->timecreated = $toolproxy->timemodified;
3085 if (!isset($toolproxy->createdby)) {
3086 $toolproxy->createdby = $USER->id;
3088 $toolproxy->guid = random_string();
3089 $toolproxy->secret = random_string();
3090 $id = $DB->insert_record('lti_tool_proxies', $toolproxy);
3093 return $id;
3097 * Updates a tool proxy in the database
3099 * @param object $toolproxy Tool proxy
3101 * @return int Record id number
3103 function lti_update_tool_proxy($toolproxy) {
3104 global $DB;
3106 $toolproxy->timemodified = time();
3107 $id = $DB->update_record('lti_tool_proxies', $toolproxy);
3109 return $id;
3113 * Delete a Tool Proxy
3115 * @param int $id Tool Proxy id
3117 function lti_delete_tool_proxy($id) {
3118 global $DB;
3119 $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id));
3120 $tools = $DB->get_records('lti_types', array('toolproxyid' => $id));
3121 foreach ($tools as $tool) {
3122 lti_delete_type($tool->id);
3124 $DB->delete_records('lti_tool_proxies', array('id' => $id));
3128 * Get both LTI tool proxies and tool types.
3130 * If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool
3131 * types.
3132 * For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types
3133 * will be returned.
3135 * @param int $limit Maximum number of tools returned.
3136 * @param int $offset Do not return tools before offset index.
3137 * @param bool $orphanedonly If true, only return orphaned proxies.
3138 * @param int $toolproxyid If not 0, only return tool types that have this tool proxy id.
3139 * @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types.
3141 function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array {
3142 global $DB;
3144 if ($orphanedonly) {
3145 $orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false);
3146 $countsql = helper::get_tool_proxy_sql($orphanedonly, true);
3147 $proxies = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit);
3148 $totalproxiescount = $DB->count_records_sql($countsql);
3149 } else {
3150 $proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC',
3151 '*', $offset, $limit);
3152 $totalproxiescount = $DB->count_records('lti_tool_proxies');
3155 // Find new offset and limit for tool types after getting proxies and set up query.
3156 $typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative.
3157 $typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative.
3158 $typesparams = [];
3159 if (!empty($toolproxyid)) {
3160 $typesparams['toolproxyid'] = $toolproxyid;
3163 $types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC',
3164 '*', $typesoffset, $typeslimit);
3166 return [$proxies, array_map('serialise_tool_type', $types)];
3170 * Get the total number of LTI tool types and tool proxies.
3172 * @param bool $orphanedonly If true, only count orphaned proxies.
3173 * @param int $toolproxyid If not 0, only count tool types that have this tool proxy id.
3174 * @return int Count of tools.
3176 function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int {
3177 global $DB;
3179 $typessql = "SELECT count(*)
3180 FROM {lti_types}";
3181 $typesparams = [];
3182 if (!empty($toolproxyid)) {
3183 $typessql .= " WHERE toolproxyid = :toolproxyid";
3184 $typesparams['toolproxyid'] = $toolproxyid;
3187 $proxiessql = helper::get_tool_proxy_sql($orphanedonly, true);
3189 $countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause();
3191 return $DB->count_records_sql($countsql, $typesparams);
3195 * Add a tool configuration in the database
3197 * @param object $config Tool configuration
3199 * @return int Record id number
3201 function lti_add_config($config) {
3202 global $DB;
3204 return $DB->insert_record('lti_types_config', $config);
3208 * Updates a tool configuration in the database
3210 * @param object $config Tool configuration
3212 * @return mixed Record id number
3214 function lti_update_config($config) {
3215 global $DB;
3217 $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));
3219 if ($old) {
3220 $config->id = $old->id;
3221 $return = $DB->update_record('lti_types_config', $config);
3222 } else {
3223 $return = $DB->insert_record('lti_types_config', $config);
3225 return $return;
3229 * Gets the tool settings
3231 * @param int $toolproxyid Id of tool proxy record (or tool ID if negative)
3232 * @param int $courseid Id of course (null if system settings)
3233 * @param int $instanceid Id of course module (null if system or context settings)
3235 * @return array Array settings
3237 function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) {
3238 global $DB;
3240 $settings = array();
3241 if ($toolproxyid > 0) {
3242 $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
3243 'course' => $courseid, 'coursemoduleid' => $instanceid));
3244 } else {
3245 $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid,
3246 'course' => $courseid, 'coursemoduleid' => $instanceid));
3248 if ($settingsstr !== false) {
3249 $settings = json_decode($settingsstr, true);
3251 return $settings;
3255 * Sets the tool settings (
3257 * @param array $settings Array of settings
3258 * @param int $toolproxyid Id of tool proxy record (or tool ID if negative)
3259 * @param int $courseid Id of course (null if system settings)
3260 * @param int $instanceid Id of course module (null if system or context settings)
3262 function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) {
3263 global $DB;
3265 $json = json_encode($settings);
3266 if ($toolproxyid >= 0) {
3267 $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
3268 'course' => $courseid, 'coursemoduleid' => $instanceid));
3269 } else {
3270 $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid,
3271 'course' => $courseid, 'coursemoduleid' => $instanceid));
3273 if ($record !== false) {
3274 $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
3275 } else {
3276 $record = new \stdClass();
3277 if ($toolproxyid > 0) {
3278 $record->toolproxyid = $toolproxyid;
3279 } else {
3280 $record->typeid = -$toolproxyid;
3282 $record->course = $courseid;
3283 $record->coursemoduleid = $instanceid;
3284 $record->settings = $json;
3285 $record->timecreated = time();
3286 $record->timemodified = $record->timecreated;
3287 $DB->insert_record('lti_tool_settings', $record);
3292 * Signs the petition to launch the external tool using OAuth
3294 * @param array $oldparms Parameters to be passed for signing
3295 * @param string $endpoint url of the external tool
3296 * @param string $method Method for sending the parameters (e.g. POST)
3297 * @param string $oauthconsumerkey
3298 * @param string $oauthconsumersecret
3299 * @return array|null
3301 function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
3303 $parms = $oldparms;
3305 $testtoken = '';
3307 // TODO: Switch to core oauthlib once implemented - MDL-30149.
3308 $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1();
3309 $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null);
3310 $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms);
3311 $accreq->sign_request($hmacmethod, $testconsumer, $testtoken);
3313 $newparms = $accreq->get_parameters();
3315 return $newparms;
3319 * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT
3321 * @param array $parms Parameters to be passed for signing
3322 * @param string $endpoint url of the external tool
3323 * @param string $oauthconsumerkey
3324 * @param string $typeid ID of LTI tool type
3325 * @param string $nonce Nonce value to use
3326 * @return array|null
3328 function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {
3329 global $CFG;
3331 if (empty($typeid)) {
3332 $typeid = 0;
3334 $messagetypemapping = lti_get_jwt_message_type_mapping();
3335 if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) {
3336 $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']];
3338 if (isset($parms['roles'])) {
3339 $roles = explode(',', $parms['roles']);
3340 $newroles = array();
3341 foreach ($roles as $role) {
3342 if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {
3343 $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);
3344 } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {
3345 $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);
3346 } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {
3347 $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);
3348 } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {
3349 $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";
3351 $newroles[] = $role;
3353 $parms['roles'] = implode(',', $newroles);
3356 $now = time();
3357 if (empty($nonce)) {
3358 $nonce = bin2hex(openssl_random_pseudo_bytes(10));
3360 $claimmapping = lti_get_jwt_claim_mapping();
3361 $payload = array(
3362 'nonce' => $nonce,
3363 'iat' => $now,
3364 'exp' => $now + 60,
3366 $payload['iss'] = $CFG->wwwroot;
3367 $payload['aud'] = $oauthconsumerkey;
3368 $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid);
3369 $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;
3371 foreach ($parms as $key => $value) {
3372 $claim = LTI_JWT_CLAIM_PREFIX;
3373 if (array_key_exists($key, $claimmapping)) {
3374 $mapping = $claimmapping[$key];
3375 $type = $mapping["type"] ?? "string";
3376 if ($mapping['isarray']) {
3377 $value = explode(',', $value);
3378 sort($value);
3379 } else if ($type == 'boolean') {
3380 $value = isset($value) && ($value == 'true');
3382 if (!empty($mapping['suffix'])) {
3383 $claim .= "-{$mapping['suffix']}";
3385 $claim .= '/claim/';
3386 if (is_null($mapping['group'])) {
3387 $payload[$mapping['claim']] = $value;
3388 } else if (empty($mapping['group'])) {
3389 $payload["{$claim}{$mapping['claim']}"] = $value;
3390 } else {
3391 $claim .= $mapping['group'];
3392 $payload[$claim][$mapping['claim']] = $value;
3394 } else if (strpos($key, 'custom_') === 0) {
3395 $payload["{$claim}/claim/custom"][substr($key, 7)] = $value;
3396 } else if (strpos($key, 'ext_') === 0) {
3397 $payload["{$claim}/claim/ext"][substr($key, 4)] = $value;
3401 $privatekey = jwks_helper::get_private_key();
3402 $jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']);
3404 $newparms = array();
3405 $newparms['id_token'] = $jwt;
3407 return $newparms;
3411 * Verfies the JWT and converts its claims to their equivalent message parameter.
3413 * @param int $typeid
3414 * @param string $jwtparam JWT parameter
3416 * @return array message parameters
3417 * @throws moodle_exception
3419 function lti_convert_from_jwt($typeid, $jwtparam) {
3421 $params = array();
3422 $parts = explode('.', $jwtparam);
3423 $ok = (count($parts) === 3);
3424 if ($ok) {
3425 $payload = JWT::urlsafeB64Decode($parts[1]);
3426 $claims = json_decode($payload, true);
3427 $ok = !is_null($claims) && !empty($claims['iss']);
3429 if ($ok) {
3430 lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);
3431 $params['oauth_consumer_key'] = $claims['iss'];
3432 foreach (lti_get_jwt_claim_mapping() as $key => $mapping) {
3433 $claim = LTI_JWT_CLAIM_PREFIX;
3434 if (!empty($mapping['suffix'])) {
3435 $claim .= "-{$mapping['suffix']}";
3437 $claim .= '/claim/';
3438 if (is_null($mapping['group'])) {
3439 $claim = $mapping['claim'];
3440 } else if (empty($mapping['group'])) {
3441 $claim .= $mapping['claim'];
3442 } else {
3443 $claim .= $mapping['group'];
3445 if (isset($claims[$claim])) {
3446 $value = null;
3447 if (empty($mapping['group'])) {
3448 $value = $claims[$claim];
3449 } else {
3450 $group = $claims[$claim];
3451 if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
3452 $value = $group[$mapping['claim']];
3455 if (!empty($value) && $mapping['isarray']) {
3456 if (is_array($value)) {
3457 if (is_array($value[0])) {
3458 $value = json_encode($value);
3459 } else {
3460 $value = implode(',', $value);
3464 if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {
3465 $params[$key] = $value;
3468 $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';
3469 if (isset($claims[$claim])) {
3470 $custom = $claims[$claim];
3471 if (is_array($custom)) {
3472 foreach ($custom as $key => $value) {
3473 $params["custom_{$key}"] = $value;
3477 $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';
3478 if (isset($claims[$claim])) {
3479 $ext = $claims[$claim];
3480 if (is_array($ext)) {
3481 foreach ($ext as $key => $value) {
3482 $params["ext_{$key}"] = $value;
3488 if (isset($params['content_items'])) {
3489 $params['content_items'] = lti_convert_content_items($params['content_items']);
3491 $messagetypemapping = lti_get_jwt_message_type_mapping();
3492 if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) {
3493 $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']];
3495 return $params;
3499 * Posts the launch petition HTML
3501 * @param array $newparms Signed parameters
3502 * @param string $endpoint URL of the external tool
3503 * @param bool $debug Debug (true/false)
3504 * @return string
3506 function lti_post_launch_html($newparms, $endpoint, $debug=false) {
3507 $r = "<form action=\"" . $endpoint .
3508 "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n";
3510 // Contruct html for the launch parameters.
3511 foreach ($newparms as $key => $value) {
3512 $key = htmlspecialchars($key, ENT_COMPAT);
3513 $value = htmlspecialchars($value, ENT_COMPAT);
3514 if ( $key == "ext_submit" ) {
3515 $r .= "<input type=\"submit\"";
3516 } else {
3517 $r .= "<input type=\"hidden\" name=\"{$key}\"";
3519 $r .= " value=\"";
3520 $r .= $value;
3521 $r .= "\"/>\n";
3524 if ( $debug ) {
3525 $r .= "<script language=\"javascript\"> \n";
3526 $r .= " //<![CDATA[ \n";
3527 $r .= "function basicltiDebugToggle() {\n";
3528 $r .= " var ele = document.getElementById(\"basicltiDebug\");\n";
3529 $r .= " if (ele.style.display == \"block\") {\n";
3530 $r .= " ele.style.display = \"none\";\n";
3531 $r .= " }\n";
3532 $r .= " else {\n";
3533 $r .= " ele.style.display = \"block\";\n";
3534 $r .= " }\n";
3535 $r .= "} \n";
3536 $r .= " //]]> \n";
3537 $r .= "</script>\n";
3538 $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";
3539 $r .= get_string("toggle_debug_data", "lti")."</a>\n";
3540 $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
3541 $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n";
3542 $r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
3543 $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n";
3544 foreach ($newparms as $key => $value) {
3545 $key = htmlspecialchars($key, ENT_COMPAT);
3546 $value = htmlspecialchars($value, ENT_COMPAT);
3547 $r .= "$key = $value<br/>\n";
3549 $r .= "&nbsp;<br/>\n";
3550 $r .= "</div>\n";
3552 $r .= "</form>\n";
3554 // Auto-submit the form if endpoint is set.
3555 if ($endpoint !== '' && !$debug) {
3556 $r .= " <script type=\"text/javascript\"> \n" .
3557 " //<![CDATA[ \n" .
3558 " document.ltiLaunchForm.submit(); \n" .
3559 " //]]> \n" .
3560 " </script> \n";
3562 return $r;
3566 * Generate the form for initiating a login request for an LTI 1.3 message
3568 * @param int $courseid Course ID
3569 * @param int $cmid LTI instance ID
3570 * @param stdClass|null $instance LTI instance
3571 * @param stdClass $config Tool type configuration
3572 * @param string $messagetype LTI message type
3573 * @param string $title Title of content item
3574 * @param string $text Description of content item
3575 * @param int $foruserid Id of the user targeted by the launch
3576 * @return string
3578 function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request',
3579 $title = '', $text = '', $foruserid = 0) {
3580 global $SESSION;
3582 $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text);
3584 $r = "<form action=\"" . $config->lti_initiatelogin .
3585 "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " .
3586 "encType=\"application/x-www-form-urlencoded\">\n";
3588 foreach ($params as $key => $value) {
3589 $key = htmlspecialchars($key, ENT_COMPAT);
3590 $value = htmlspecialchars($value, ENT_COMPAT);
3591 $r .= " <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
3593 $r .= "</form>\n";
3595 $r .= "<script type=\"text/javascript\">\n" .
3596 "//<![CDATA[\n" .
3597 "document.ltiInitiateLoginForm.submit();\n" .
3598 "//]]>\n" .
3599 "</script>\n";
3601 return $r;
3605 * Prepares an LTI 1.3 login request
3607 * @param int $courseid Course ID
3608 * @param int $cmid Course Module instance ID
3609 * @param stdClass|null $instance LTI instance
3610 * @param stdClass $config Tool type configuration
3611 * @param string $messagetype LTI message type
3612 * @param int $foruserid Id of the user targeted by the launch
3613 * @param string $title Title of content item
3614 * @param string $text Description of content item
3615 * @return array Login request parameters
3617 function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') {
3618 global $USER, $CFG, $SESSION;
3619 $ltihint = [];
3620 if (!empty($instance)) {
3621 $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;
3622 $launchid = 'ltilaunch'.$instance->id.'_'.rand();
3623 $ltihint['cmid'] = $cmid;
3624 $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,";
3625 } else {
3626 $endpoint = $config->lti_toolurl;
3627 if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) {
3628 $endpoint = $config->lti_toolurl_ContentItemSelectionRequest;
3630 $launchid = "ltilaunch_$messagetype".rand();
3631 $SESSION->$launchid =
3632 "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text);
3634 $endpoint = trim($endpoint);
3635 $services = lti_get_services();
3636 foreach ($services as $service) {
3637 [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance);
3640 $ltihint['launchid'] = $launchid;
3641 // If SSL is forced make sure https is on the normal launch URL.
3642 if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {
3643 $endpoint = lti_ensure_url_is_https($endpoint);
3644 } else if (!strstr($endpoint, '://')) {
3645 $endpoint = 'http://' . $endpoint;
3648 $params = array();
3649 $params['iss'] = $CFG->wwwroot;
3650 $params['target_link_uri'] = $endpoint;
3651 $params['login_hint'] = $USER->id;
3652 $params['lti_message_hint'] = json_encode($ltihint);
3653 $params['client_id'] = $config->lti_clientid;
3654 $params['lti_deployment_id'] = $config->typeid;
3655 return $params;
3658 function lti_get_type($typeid) {
3659 global $DB;
3661 return $DB->get_record('lti_types', array('id' => $typeid));
3664 function lti_get_launch_container($lti, $toolconfig) {
3665 if (empty($lti->launchcontainer)) {
3666 $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
3669 if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3670 if (isset($toolconfig['launchcontainer'])) {
3671 $launchcontainer = $toolconfig['launchcontainer'];
3673 } else {
3674 $launchcontainer = $lti->launchcontainer;
3677 if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3678 $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
3681 $devicetype = core_useragent::get_device_type();
3683 // Scrolling within the object element doesn't work on iOS or Android
3684 // Opening the popup window also had some issues in testing
3685 // For mobile devices, always take up the entire screen to ensure the best experience.
3686 if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) {
3687 $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW;
3690 return $launchcontainer;
3693 function lti_request_is_using_ssl() {
3694 global $CFG;
3695 return (stripos($CFG->wwwroot, 'https://') === 0);
3698 function lti_ensure_url_is_https($url) {
3699 if (!strstr($url, '://')) {
3700 $url = 'https://' . $url;
3701 } else {
3702 // If the URL starts with http, replace with https.
3703 if (stripos($url, 'http://') === 0) {
3704 $url = 'https://' . substr($url, 7);
3708 return $url;
3712 * Determines if we should try to log the request
3714 * @param string $rawbody
3715 * @return bool
3717 function lti_should_log_request($rawbody) {
3718 global $CFG;
3720 if (empty($CFG->mod_lti_log_users)) {
3721 return false;
3724 $logusers = explode(',', $CFG->mod_lti_log_users);
3725 if (empty($logusers)) {
3726 return false;
3729 try {
3730 $xml = new \SimpleXMLElement($rawbody);
3731 $ns = $xml->getNamespaces();
3732 $ns = array_shift($ns);
3733 $xml->registerXPathNamespace('lti', $ns);
3734 $requestuserid = '';
3735 if ($node = $xml->xpath('//lti:userId')) {
3736 $node = $node[0];
3737 $requestuserid = clean_param((string) $node, PARAM_INT);
3738 } else if ($node = $xml->xpath('//lti:sourcedId')) {
3739 $node = $node[0];
3740 $resultjson = json_decode((string) $node);
3741 $requestuserid = clean_param($resultjson->data->userid, PARAM_INT);
3743 } catch (Exception $e) {
3744 return false;
3747 if (empty($requestuserid) or !in_array($requestuserid, $logusers)) {
3748 return false;
3751 return true;
3755 * Logs the request to a file in temp dir.
3757 * @param string $rawbody
3759 function lti_log_request($rawbody) {
3760 if ($tempdir = make_temp_directory('mod_lti', false)) {
3761 if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
3762 $content = "Request Headers:\n";
3763 foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
3764 $content .= "$header: $value\n";
3766 $content .= "Request Body:\n";
3767 $content .= $rawbody;
3769 file_put_contents($tempfile, $content);
3770 chmod($tempfile, 0644);
3776 * Log an LTI response.
3778 * @param string $responsexml The response XML
3779 * @param Exception $e If there was an exception, pass that too
3781 function lti_log_response($responsexml, $e = null) {
3782 if ($tempdir = make_temp_directory('mod_lti', false)) {
3783 if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
3784 $content = '';
3785 if ($e instanceof Exception) {
3786 $info = get_exception_info($e);
3788 $content .= "Exception:\n";
3789 $content .= "Message: $info->message\n";
3790 $content .= "Debug info: $info->debuginfo\n";
3791 $content .= "Backtrace:\n";
3792 $content .= format_backtrace($info->backtrace, true);
3793 $content .= "\n";
3795 $content .= "Response XML:\n";
3796 $content .= $responsexml;
3798 file_put_contents($tempfile, $content);
3799 chmod($tempfile, 0644);
3805 * Fetches LTI type configuration for an LTI instance
3807 * @param stdClass $instance
3808 * @return array Can be empty if no type is found
3810 function lti_get_type_config_by_instance($instance) {
3811 $typeid = null;
3812 if (empty($instance->typeid)) {
3813 $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
3814 if ($tool) {
3815 $typeid = $tool->id;
3817 } else {
3818 $typeid = $instance->typeid;
3820 if (!empty($typeid)) {
3821 return lti_get_type_config($typeid);
3823 return array();
3827 * Enforce type config settings onto the LTI instance
3829 * @param stdClass $instance
3830 * @param array $typeconfig
3832 function lti_force_type_config_settings($instance, array $typeconfig) {
3833 $forced = array(
3834 'instructorchoicesendname' => 'sendname',
3835 'instructorchoicesendemailaddr' => 'sendemailaddr',
3836 'instructorchoiceacceptgrades' => 'acceptgrades',
3839 foreach ($forced as $instanceparam => $typeconfigparam) {
3840 if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) {
3841 $instance->$instanceparam = $typeconfig[$typeconfigparam];
3847 * Initializes an array with the capabilities supported by the LTI module
3849 * @return array List of capability names (without a dollar sign prefix)
3851 function lti_get_capabilities() {
3853 $capabilities = array(
3854 'basic-lti-launch-request' => '',
3855 'ContentItemSelectionRequest' => '',
3856 'ToolProxyRegistrationRequest' => '',
3857 'Context.id' => 'context_id',
3858 'Context.title' => 'context_title',
3859 'Context.label' => 'context_label',
3860 'Context.id.history' => null,
3861 'Context.sourcedId' => 'lis_course_section_sourcedid',
3862 'Context.longDescription' => '$COURSE->summary',
3863 'Context.timeFrame.begin' => '$COURSE->startdate',
3864 'CourseSection.title' => 'context_title',
3865 'CourseSection.label' => 'context_label',
3866 'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
3867 'CourseSection.longDescription' => '$COURSE->summary',
3868 'CourseSection.timeFrame.begin' => null,
3869 'CourseSection.timeFrame.end' => null,
3870 'ResourceLink.id' => 'resource_link_id',
3871 'ResourceLink.title' => 'resource_link_title',
3872 'ResourceLink.description' => 'resource_link_description',
3873 'User.id' => 'user_id',
3874 'User.username' => '$USER->username',
3875 'Person.name.full' => 'lis_person_name_full',
3876 'Person.name.given' => 'lis_person_name_given',
3877 'Person.name.family' => 'lis_person_name_family',
3878 'Person.email.primary' => 'lis_person_contact_email_primary',
3879 'Person.sourcedId' => 'lis_person_sourcedid',
3880 'Person.name.middle' => '$USER->middlename',
3881 'Person.address.street1' => '$USER->address',
3882 'Person.address.locality' => '$USER->city',
3883 'Person.address.country' => '$USER->country',
3884 'Person.address.timezone' => '$USER->timezone',
3885 'Person.phone.primary' => '$USER->phone1',
3886 'Person.phone.mobile' => '$USER->phone2',
3887 'Person.webaddress' => '$USER->url',
3888 'Membership.role' => 'roles',
3889 'Result.sourcedId' => 'lis_result_sourcedid',
3890 'Result.autocreate' => 'lis_outcome_service_url',
3891 'BasicOutcome.sourcedId' => 'lis_result_sourcedid',
3892 'BasicOutcome.url' => 'lis_outcome_service_url',
3893 'Moodle.Person.userGroupIds' => null);
3895 return $capabilities;
3900 * Initializes an array with the services supported by the LTI module
3902 * @return array List of services
3904 function lti_get_services() {
3906 $services = array();
3907 $definedservices = core_component::get_plugin_list('ltiservice');
3908 foreach ($definedservices as $name => $location) {
3909 $classname = "\\ltiservice_{$name}\\local\\service\\{$name}";
3910 $services[] = new $classname();
3913 return $services;
3918 * Initializes an instance of the named service
3920 * @param string $servicename Name of service
3922 * @return bool|\mod_lti\local\ltiservice\service_base Service
3924 function lti_get_service_by_name($servicename) {
3926 $service = false;
3927 $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}";
3928 if (class_exists($classname)) {
3929 $service = new $classname();
3932 return $service;
3937 * Finds a service by id
3939 * @param \mod_lti\local\ltiservice\service_base[] $services Array of services
3940 * @param string $resourceid ID of resource
3942 * @return mod_lti\local\ltiservice\service_base Service
3944 function lti_get_service_by_resource_id($services, $resourceid) {
3946 $service = false;
3947 foreach ($services as $aservice) {
3948 foreach ($aservice->get_resources() as $resource) {
3949 if ($resource->get_id() === $resourceid) {
3950 $service = $aservice;
3951 break 2;
3956 return $service;
3961 * Initializes an array with the scopes for services supported by the LTI module
3962 * and authorized for this particular tool instance.
3964 * @param object $type LTI tool type
3965 * @param array $typeconfig LTI tool type configuration
3967 * @return array List of scopes
3969 function lti_get_permitted_service_scopes($type, $typeconfig) {
3971 $services = lti_get_services();
3972 $scopes = array();
3973 foreach ($services as $service) {
3974 $service->set_type($type);
3975 $service->set_typeconfig($typeconfig);
3976 $servicescopes = $service->get_permitted_scopes();
3977 if (!empty($servicescopes)) {
3978 $scopes = array_merge($scopes, $servicescopes);
3982 return $scopes;
3986 * Extracts the named contexts from a tool proxy
3988 * @param object $json
3990 * @return array Contexts
3992 function lti_get_contexts($json) {
3994 $contexts = array();
3995 if (isset($json->{'@context'})) {
3996 foreach ($json->{'@context'} as $context) {
3997 if (is_object($context)) {
3998 $contexts = array_merge(get_object_vars($context), $contexts);
4003 return $contexts;
4008 * Converts an ID to a fully-qualified ID
4010 * @param array $contexts
4011 * @param string $id
4013 * @return string Fully-qualified ID
4015 function lti_get_fqid($contexts, $id) {
4017 $parts = explode(':', $id, 2);
4018 if (count($parts) > 1) {
4019 if (array_key_exists($parts[0], $contexts)) {
4020 $id = $contexts[$parts[0]] . $parts[1];
4024 return $id;
4029 * Returns the icon for the given tool type
4031 * @param stdClass $type The tool type
4033 * @return string The url to the tool type's corresponding icon
4035 function get_tool_type_icon_url(stdClass $type) {
4036 global $OUTPUT;
4038 $iconurl = $type->secureicon;
4040 if (empty($iconurl)) {
4041 $iconurl = $type->icon;
4044 if (empty($iconurl)) {
4045 $iconurl = $OUTPUT->image_url('monologo', 'lti')->out();
4048 return $iconurl;
4052 * Returns the edit url for the given tool type
4054 * @param stdClass $type The tool type
4056 * @return string The url to edit the tool type
4058 function get_tool_type_edit_url(stdClass $type) {
4059 $url = new moodle_url('/mod/lti/typessettings.php',
4060 array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4061 return $url->out();
4065 * Returns the edit url for the given tool proxy.
4067 * @param stdClass $proxy The tool proxy
4069 * @return string The url to edit the tool type
4071 function get_tool_proxy_edit_url(stdClass $proxy) {
4072 $url = new moodle_url('/mod/lti/registersettings.php',
4073 array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
4074 return $url->out();
4078 * Returns the course url for the given tool type
4080 * @param stdClass $type The tool type
4082 * @return string The url to the course of the tool type, void if it is a site wide type
4084 function get_tool_type_course_url(stdClass $type) {
4085 if ($type->course != 1) {
4086 $url = new moodle_url('/course/view.php', array('id' => $type->course));
4087 return $url->out();
4089 return null;
4093 * Returns the icon and edit urls for the tool type and the course url if it is a course type.
4095 * @param stdClass $type The tool type
4097 * @return array The urls of the tool type
4099 function get_tool_type_urls(stdClass $type) {
4100 $courseurl = get_tool_type_course_url($type);
4102 $urls = array(
4103 'icon' => get_tool_type_icon_url($type),
4104 'edit' => get_tool_type_edit_url($type),
4107 if ($courseurl) {
4108 $urls['course'] = $courseurl;
4111 $url = new moodle_url('/mod/lti/certs.php');
4112 $urls['publickeyset'] = $url->out();
4113 $url = new moodle_url('/mod/lti/token.php');
4114 $urls['accesstoken'] = $url->out();
4115 $url = new moodle_url('/mod/lti/auth.php');
4116 $urls['authrequest'] = $url->out();
4118 return $urls;
4122 * Returns the icon and edit urls for the tool proxy.
4124 * @param stdClass $proxy The tool proxy
4126 * @return array The urls of the tool proxy
4128 function get_tool_proxy_urls(stdClass $proxy) {
4129 global $OUTPUT;
4131 $urls = array(
4132 'icon' => $OUTPUT->image_url('monologo', 'lti')->out(),
4133 'edit' => get_tool_proxy_edit_url($proxy),
4136 return $urls;
4140 * Returns information on the current state of the tool type
4142 * @param stdClass $type The tool type
4144 * @return array An array with a text description of the state, and boolean for whether it is in each state:
4145 * pending, configured, rejected, unknown
4147 function get_tool_type_state_info(stdClass $type) {
4148 $isconfigured = false;
4149 $ispending = false;
4150 $isrejected = false;
4151 $isunknown = false;
4152 switch ($type->state) {
4153 case LTI_TOOL_STATE_CONFIGURED:
4154 $state = get_string('active', 'mod_lti');
4155 $isconfigured = true;
4156 break;
4157 case LTI_TOOL_STATE_PENDING:
4158 $state = get_string('pending', 'mod_lti');
4159 $ispending = true;
4160 break;
4161 case LTI_TOOL_STATE_REJECTED:
4162 $state = get_string('rejected', 'mod_lti');
4163 $isrejected = true;
4164 break;
4165 default:
4166 $state = get_string('unknownstate', 'mod_lti');
4167 $isunknown = true;
4168 break;
4171 return array(
4172 'text' => $state,
4173 'pending' => $ispending,
4174 'configured' => $isconfigured,
4175 'rejected' => $isrejected,
4176 'unknown' => $isunknown
4181 * Returns information on the configuration of the tool type
4183 * @param stdClass $type The tool type
4185 * @return array An array with configuration details
4187 function get_tool_type_config($type) {
4188 global $CFG;
4189 $platformid = $CFG->wwwroot;
4190 $clientid = $type->clientid;
4191 $deploymentid = $type->id;
4192 $publickeyseturl = new moodle_url('/mod/lti/certs.php');
4193 $publickeyseturl = $publickeyseturl->out();
4195 $accesstokenurl = new moodle_url('/mod/lti/token.php');
4196 $accesstokenurl = $accesstokenurl->out();
4198 $authrequesturl = new moodle_url('/mod/lti/auth.php');
4199 $authrequesturl = $authrequesturl->out();
4201 return array(
4202 'platformid' => $platformid,
4203 'clientid' => $clientid,
4204 'deploymentid' => $deploymentid,
4205 'publickeyseturl' => $publickeyseturl,
4206 'accesstokenurl' => $accesstokenurl,
4207 'authrequesturl' => $authrequesturl
4212 * Returns a summary of each LTI capability this tool type requires in plain language
4214 * @param stdClass $type The tool type
4216 * @return array An array of text descriptions of each of the capabilities this tool type requires
4218 function get_tool_type_capability_groups($type) {
4219 $capabilities = lti_get_enabled_capabilities($type);
4220 $groups = array();
4221 $hascourse = false;
4222 $hasactivities = false;
4223 $hasuseraccount = false;
4224 $hasuserpersonal = false;
4226 foreach ($capabilities as $capability) {
4227 // Bail out early if we've already found all groups.
4228 if (count($groups) >= 4) {
4229 continue;
4232 if (!$hascourse && preg_match('/^CourseSection/', $capability)) {
4233 $hascourse = true;
4234 $groups[] = get_string('courseinformation', 'mod_lti');
4235 } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) {
4236 $hasactivities = true;
4237 $groups[] = get_string('courseactivitiesorresources', 'mod_lti');
4238 } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) {
4239 $hasuseraccount = true;
4240 $groups[] = get_string('useraccountinformation', 'mod_lti');
4241 } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) {
4242 $hasuserpersonal = true;
4243 $groups[] = get_string('userpersonalinformation', 'mod_lti');
4247 return $groups;
4252 * Returns the ids of each instance of this tool type
4254 * @param stdClass $type The tool type
4256 * @return array An array of ids of the instances of this tool type
4258 function get_tool_type_instance_ids($type) {
4259 global $DB;
4261 return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id)));
4265 * Serialises this tool type
4267 * @param stdClass $type The tool type
4269 * @return array An array of values representing this type
4271 function serialise_tool_type(stdClass $type) {
4272 global $CFG;
4274 $capabilitygroups = get_tool_type_capability_groups($type);
4275 $instanceids = get_tool_type_instance_ids($type);
4276 // Clean the name. We don't want tags here.
4277 $name = clean_param($type->name, PARAM_NOTAGS);
4278 if (!empty($type->description)) {
4279 // Clean the description. We don't want tags here.
4280 $description = clean_param($type->description, PARAM_NOTAGS);
4281 } else {
4282 $description = get_string('editdescription', 'mod_lti');
4284 return array(
4285 'id' => $type->id,
4286 'name' => $name,
4287 'description' => $description,
4288 'urls' => get_tool_type_urls($type),
4289 'state' => get_tool_type_state_info($type),
4290 'platformid' => $CFG->wwwroot,
4291 'clientid' => $type->clientid,
4292 'deploymentid' => $type->id,
4293 'hascapabilitygroups' => !empty($capabilitygroups),
4294 'capabilitygroups' => $capabilitygroups,
4295 // Course ID of 1 means it's not linked to a course.
4296 'courseid' => $type->course == 1 ? 0 : $type->course,
4297 'instanceids' => $instanceids,
4298 'instancecount' => count($instanceids)
4303 * Loads the cartridge information into the tool type, if the launch url is for a cartridge file
4305 * @param stdClass $type The tool type object to be filled in
4306 * @since Moodle 3.1
4308 function lti_load_type_if_cartridge($type) {
4309 if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) {
4310 lti_load_type_from_cartridge($type->lti_toolurl, $type);
4315 * Loads the cartridge information into the new tool, if the launch url is for a cartridge file
4317 * @param stdClass $lti The tools config
4318 * @since Moodle 3.1
4320 function lti_load_tool_if_cartridge($lti) {
4321 if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) {
4322 lti_load_tool_from_cartridge($lti->toolurl, $lti);
4327 * Determines if the given url is for a IMS basic cartridge
4329 * @param string $url The url to be checked
4330 * @return True if the url is for a cartridge
4331 * @since Moodle 3.1
4333 function lti_is_cartridge($url) {
4334 // If it is empty, it's not a cartridge.
4335 if (empty($url)) {
4336 return false;
4338 // If it has xml at the end of the url, it's a cartridge.
4339 if (preg_match('/\.xml$/', $url)) {
4340 return true;
4342 // Even if it doesn't have .xml, load the url to check if it's a cartridge..
4343 try {
4344 $toolinfo = lti_load_cartridge($url,
4345 array(
4346 "launch_url" => "launchurl"
4349 if (!empty($toolinfo['launchurl'])) {
4350 return true;
4352 } catch (moodle_exception $e) {
4353 return false; // Error loading the xml, so it's not a cartridge.
4355 return false;
4359 * Allows you to load settings for an external tool type from an IMS cartridge.
4361 * @param string $url The URL to the cartridge
4362 * @param stdClass $type The tool type object to be filled in
4363 * @throws moodle_exception if the cartridge could not be loaded correctly
4364 * @since Moodle 3.1
4366 function lti_load_type_from_cartridge($url, $type) {
4367 $toolinfo = lti_load_cartridge($url,
4368 array(
4369 "title" => "lti_typename",
4370 "launch_url" => "lti_toolurl",
4371 "description" => "lti_description",
4372 "icon" => "lti_icon",
4373 "secure_icon" => "lti_secureicon"
4375 array(
4376 "icon_url" => "lti_extension_icon",
4377 "secure_icon_url" => "lti_extension_secureicon"
4380 // If an activity name exists, unset the cartridge name so we don't override it.
4381 if (isset($type->lti_typename)) {
4382 unset($toolinfo['lti_typename']);
4385 // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4386 if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
4387 $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
4389 unset($toolinfo['lti_extension_icon']);
4391 if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
4392 $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
4394 unset($toolinfo['lti_extension_secureicon']);
4396 // Ensure Custom icons aren't overridden by cartridge params.
4397 if (!empty($type->lti_icon)) {
4398 unset($toolinfo['lti_icon']);
4401 if (!empty($type->lti_secureicon)) {
4402 unset($toolinfo['lti_secureicon']);
4405 foreach ($toolinfo as $property => $value) {
4406 $type->$property = $value;
4411 * Allows you to load in the configuration for an external tool from an IMS cartridge.
4413 * @param string $url The URL to the cartridge
4414 * @param stdClass $lti LTI object
4415 * @throws moodle_exception if the cartridge could not be loaded correctly
4416 * @since Moodle 3.1
4418 function lti_load_tool_from_cartridge($url, $lti) {
4419 $toolinfo = lti_load_cartridge($url,
4420 array(
4421 "title" => "name",
4422 "launch_url" => "toolurl",
4423 "secure_launch_url" => "securetoolurl",
4424 "description" => "intro",
4425 "icon" => "icon",
4426 "secure_icon" => "secureicon"
4428 array(
4429 "icon_url" => "extension_icon",
4430 "secure_icon_url" => "extension_secureicon"
4433 // If an activity name exists, unset the cartridge name so we don't override it.
4434 if (isset($lti->name)) {
4435 unset($toolinfo['name']);
4438 // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
4439 if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
4440 $toolinfo['icon'] = $toolinfo['extension_icon'];
4442 unset($toolinfo['extension_icon']);
4444 if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
4445 $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
4447 unset($toolinfo['extension_secureicon']);
4449 foreach ($toolinfo as $property => $value) {
4450 $lti->$property = $value;
4455 * Search for a tag within an XML DOMDocument
4457 * @param string $url The url of the cartridge to be loaded
4458 * @param array $map The map of tags to keys in the return array
4459 * @param array $propertiesmap The map of properties to keys in the return array
4460 * @return array An associative array with the given keys and their values from the cartridge
4461 * @throws moodle_exception if the cartridge could not be loaded correctly
4462 * @since Moodle 3.1
4464 function lti_load_cartridge($url, $map, $propertiesmap = array()) {
4465 global $CFG;
4466 require_once($CFG->libdir. "/filelib.php");
4468 $curl = new curl();
4469 $response = $curl->get($url);
4471 // Got a completely empty response (real or error), cannot process this with
4472 // DOMDocument::loadXML() because it errors with ValueError. So let's throw
4473 // the moodle_exception before waiting to examine the errors later.
4474 if (trim($response) === '') {
4475 throw new moodle_exception('errorreadingfile', '', '', $url);
4478 // TODO MDL-46023 Replace this code with a call to the new library.
4479 $origerrors = libxml_use_internal_errors(true);
4480 libxml_clear_errors();
4482 $document = new DOMDocument();
4483 @$document->loadXML($response, LIBXML_NONET);
4485 $cartridge = new DomXpath($document);
4487 $errors = libxml_get_errors();
4489 libxml_clear_errors();
4490 libxml_use_internal_errors($origerrors);
4492 if (count($errors) > 0) {
4493 $message = 'Failed to load cartridge.';
4494 foreach ($errors as $error) {
4495 $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line;
4497 throw new moodle_exception('errorreadingfile', '', '', $url, $message);
4500 $toolinfo = array();
4501 foreach ($map as $tag => $key) {
4502 $value = get_tag($tag, $cartridge);
4503 if ($value) {
4504 $toolinfo[$key] = $value;
4507 if (!empty($propertiesmap)) {
4508 foreach ($propertiesmap as $property => $key) {
4509 $value = get_tag("property", $cartridge, $property);
4510 if ($value) {
4511 $toolinfo[$key] = $value;
4516 return $toolinfo;
4520 * Search for a tag within an XML DOMDocument
4522 * @param stdClass $tagname The name of the tag to search for
4523 * @param XPath $xpath The XML to find the tag in
4524 * @param XPath $attribute The attribute to search for (if we should search for a child node with the given
4525 * value for the name attribute
4526 * @since Moodle 3.1
4528 function get_tag($tagname, $xpath, $attribute = null) {
4529 if ($attribute) {
4530 $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]');
4531 } else {
4532 $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']');
4534 if ($result->length > 0) {
4535 return $result->item(0)->nodeValue;
4537 return null;
4541 * Create a new access token.
4543 * @param int $typeid Tool type ID
4544 * @param string[] $scopes Scopes permitted for new token
4546 * @return stdClass Access token
4548 function lti_new_access_token($typeid, $scopes) {
4549 global $DB;
4551 // Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
4552 $numtries = 0;
4553 do {
4554 $numtries ++;
4555 $generatedtoken = md5(uniqid(rand(), 1));
4556 if ($numtries > 5) {
4557 throw new moodle_exception('Failed to generate LTI access token');
4559 } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken)));
4560 $newtoken = new stdClass();
4561 $newtoken->typeid = $typeid;
4562 $newtoken->scope = json_encode(array_values($scopes));
4563 $newtoken->token = $generatedtoken;
4565 $newtoken->timecreated = time();
4566 $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE;
4567 $newtoken->lastaccess = null;
4569 $DB->insert_record('lti_access_tokens', $newtoken);
4571 return $newtoken;
4577 * Wrapper for function libxml_disable_entity_loader() deprecated in PHP 8
4579 * Method was deprecated in PHP 8 and it shows deprecation message. However it is still
4580 * required in the previous versions on PHP. While Moodle supports both PHP 7 and 8 we need to keep it.
4581 * @see https://php.watch/versions/8.0/libxml_disable_entity_loader-deprecation
4583 * @param bool $value
4584 * @return bool
4586 * @deprecated since Moodle 4.3
4588 function lti_libxml_disable_entity_loader(bool $value): bool {
4589 debugging(__FUNCTION__ . '() is deprecated, please do not use it any more', DEBUG_DEVELOPER);
4590 return true;