Merge branch 'MDL-68384-fix-spec-violations-310' of https://github.com/cengage/moodle...
[moodle.git] / mod / lti / locallib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 //
17 // This file is part of BasicLTI4Moodle
18 //
19 // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
20 // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
21 // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
22 // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
23 // are already supporting or going to support BasicLTI. This project Implements the consumer
24 // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
25 // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
26 // at the GESSI research group at UPC.
27 // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
28 // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
29 // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
30 //
31 // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
32 // of the Universitat Politecnica de Catalunya http://www.upc.edu
33 // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu.
35 /**
36  * This file contains the library of functions and constants for the lti module
37  *
38  * @package mod_lti
39  * @copyright  2009 Marc Alier, Jordi Piguillem, Nikolas Galanis
40  *  marc.alier@upc.edu
41  * @copyright  2009 Universitat Politecnica de Catalunya http://www.upc.edu
42  * @author     Marc Alier
43  * @author     Jordi Piguillem
44  * @author     Nikolas Galanis
45  * @author     Chris Scribner
46  * @copyright  2015 Vital Source Technologies http://vitalsource.com
47  * @author     Stephen Vickers
48  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49  */
51 defined('MOODLE_INTERNAL') || die;
53 // TODO: Switch to core oauthlib once implemented - MDL-30149.
54 use moodle\mod\lti as lti;
55 use Firebase\JWT\JWT;
56 use Firebase\JWT\JWK;
58 global $CFG;
59 require_once($CFG->dirroot.'/mod/lti/OAuth.php');
60 require_once($CFG->libdir.'/weblib.php');
61 require_once($CFG->dirroot . '/course/modlib.php');
62 require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
64 define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i');
66 define('LTI_LAUNCH_CONTAINER_DEFAULT', 1);
67 define('LTI_LAUNCH_CONTAINER_EMBED', 2);
68 define('LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS', 3);
69 define('LTI_LAUNCH_CONTAINER_WINDOW', 4);
70 define('LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW', 5);
72 define('LTI_TOOL_STATE_ANY', 0);
73 define('LTI_TOOL_STATE_CONFIGURED', 1);
74 define('LTI_TOOL_STATE_PENDING', 2);
75 define('LTI_TOOL_STATE_REJECTED', 3);
76 define('LTI_TOOL_PROXY_TAB', 4);
78 define('LTI_TOOL_PROXY_STATE_CONFIGURED', 1);
79 define('LTI_TOOL_PROXY_STATE_PENDING', 2);
80 define('LTI_TOOL_PROXY_STATE_ACCEPTED', 3);
81 define('LTI_TOOL_PROXY_STATE_REJECTED', 4);
83 define('LTI_SETTING_NEVER', 0);
84 define('LTI_SETTING_ALWAYS', 1);
85 define('LTI_SETTING_DELEGATE', 2);
87 define('LTI_COURSEVISIBLE_NO', 0);
88 define('LTI_COURSEVISIBLE_PRECONFIGURED', 1);
89 define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2);
91 define('LTI_VERSION_1', 'LTI-1p0');
92 define('LTI_VERSION_2', 'LTI-2p0');
93 define('LTI_VERSION_1P3', '1.3.0');
94 define('LTI_RSA_KEY', 'RSA_KEY');
95 define('LTI_JWK_KEYSET', 'JWK_KEYSET');
97 define('LTI_DEFAULT_ORGID_SITEID', 'SITEID');
98 define('LTI_DEFAULT_ORGID_SITEHOST', 'SITEHOST');
100 define('LTI_ACCESS_TOKEN_LIFE', 3600);
102 // Standard prefix for JWT claims.
103 define('LTI_JWT_CLAIM_PREFIX', 'https://purl.imsglobal.org/spec/lti');
105 /**
106  * Return the mapping for standard message types to JWT message_type claim.
107  *
108  * @return array
109  */
110 function lti_get_jwt_message_type_mapping() {
111     return array(
112         'basic-lti-launch-request' => 'LtiResourceLinkRequest',
113         'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest',
114         'LtiDeepLinkingResponse' => 'ContentItemSelection',
115     );
118 /**
119  * Return the mapping for standard message parameters to JWT claim.
120  *
121  * @return array
122  */
123 function lti_get_jwt_claim_mapping() {
124     return array(
125         'accept_copy_advice' => [
126             'suffix' => 'dl',
127             'group' => 'deep_linking_settings',
128             'claim' => 'accept_copy_advice',
129             'isarray' => false,
130             'type' => 'boolean'
131         ],
132         'accept_media_types' => [
133             'suffix' => 'dl',
134             'group' => 'deep_linking_settings',
135             'claim' => 'accept_media_types',
136             'isarray' => true
137         ],
138         'accept_multiple' => [
139             'suffix' => 'dl',
140             'group' => 'deep_linking_settings',
141             'claim' => 'accept_multiple',
142             'isarray' => false,
143             'type' => 'boolean'
144         ],
145         'accept_presentation_document_targets' => [
146             'suffix' => 'dl',
147             'group' => 'deep_linking_settings',
148             'claim' => 'accept_presentation_document_targets',
149             'isarray' => true
150         ],
151         'accept_types' => [
152             'suffix' => 'dl',
153             'group' => 'deep_linking_settings',
154             'claim' => 'accept_types',
155             'isarray' => true
156         ],
157         'accept_unsigned' => [
158             'suffix' => 'dl',
159             'group' => 'deep_linking_settings',
160             'claim' => 'accept_unsigned',
161             'isarray' => false,
162             'type' => 'boolean'
163         ],
164         'auto_create' => [
165             'suffix' => 'dl',
166             'group' => 'deep_linking_settings',
167             'claim' => 'auto_create',
168             'isarray' => false,
169             'type' => 'boolean'
170         ],
171         'can_confirm' => [
172             'suffix' => 'dl',
173             'group' => 'deep_linking_settings',
174             'claim' => 'can_confirm',
175             'isarray' => false,
176             'type' => 'boolean'
177         ],
178         'content_item_return_url' => [
179             'suffix' => 'dl',
180             'group' => 'deep_linking_settings',
181             'claim' => 'deep_link_return_url',
182             'isarray' => false
183         ],
184         'content_items' => [
185             'suffix' => 'dl',
186             'group' => '',
187             'claim' => 'content_items',
188             'isarray' => true
189         ],
190         'data' => [
191             'suffix' => 'dl',
192             'group' => 'deep_linking_settings',
193             'claim' => 'data',
194             'isarray' => false
195         ],
196         'text' => [
197             'suffix' => 'dl',
198             'group' => 'deep_linking_settings',
199             'claim' => 'text',
200             'isarray' => false
201         ],
202         'title' => [
203             'suffix' => 'dl',
204             'group' => 'deep_linking_settings',
205             'claim' => 'title',
206             'isarray' => false
207         ],
208         'lti_msg' => [
209             'suffix' => 'dl',
210             'group' => '',
211             'claim' => 'msg',
212             'isarray' => false
213         ],
214         'lti_log' => [
215             'suffix' => 'dl',
216             'group' => '',
217             'claim' => 'log',
218             'isarray' => false
219         ],
220         'lti_errormsg' => [
221             'suffix' => 'dl',
222             'group' => '',
223             'claim' => 'errormsg',
224             'isarray' => false
225         ],
226         'lti_errorlog' => [
227             'suffix' => 'dl',
228             'group' => '',
229             'claim' => 'errorlog',
230             'isarray' => false
231         ],
232         'context_id' => [
233             'suffix' => '',
234             'group' => 'context',
235             'claim' => 'id',
236             'isarray' => false
237         ],
238         'context_label' => [
239             'suffix' => '',
240             'group' => 'context',
241             'claim' => 'label',
242             'isarray' => false
243         ],
244         'context_title' => [
245             'suffix' => '',
246             'group' => 'context',
247             'claim' => 'title',
248             'isarray' => false
249         ],
250         'context_type' => [
251             'suffix' => '',
252             'group' => 'context',
253             'claim' => 'type',
254             'isarray' => true
255         ],
256         'lis_course_offering_sourcedid' => [
257             'suffix' => '',
258             'group' => 'lis',
259             'claim' => 'course_offering_sourcedid',
260             'isarray' => false
261         ],
262         'lis_course_section_sourcedid' => [
263             'suffix' => '',
264             'group' => 'lis',
265             'claim' => 'course_section_sourcedid',
266             'isarray' => false
267         ],
268         'launch_presentation_css_url' => [
269             'suffix' => '',
270             'group' => 'launch_presentation',
271             'claim' => 'css_url',
272             'isarray' => false
273         ],
274         'launch_presentation_document_target' => [
275             'suffix' => '',
276             'group' => 'launch_presentation',
277             'claim' => 'document_target',
278             'isarray' => false
279         ],
280         'launch_presentation_height' => [
281             'suffix' => '',
282             'group' => 'launch_presentation',
283             'claim' => 'height',
284             'isarray' => false
285         ],
286         'launch_presentation_locale' => [
287             'suffix' => '',
288             'group' => 'launch_presentation',
289             'claim' => 'locale',
290             'isarray' => false
291         ],
292         'launch_presentation_return_url' => [
293             'suffix' => '',
294             'group' => 'launch_presentation',
295             'claim' => 'return_url',
296             'isarray' => false
297         ],
298         'launch_presentation_width' => [
299             'suffix' => '',
300             'group' => 'launch_presentation',
301             'claim' => 'width',
302             'isarray' => false
303         ],
304         'lis_person_contact_email_primary' => [
305             'suffix' => '',
306             'group' => null,
307             'claim' => 'email',
308             'isarray' => false
309         ],
310         'lis_person_name_family' => [
311             'suffix' => '',
312             'group' => null,
313             'claim' => 'family_name',
314             'isarray' => false
315         ],
316         'lis_person_name_full' => [
317             'suffix' => '',
318             'group' => null,
319             'claim' => 'name',
320             'isarray' => false
321         ],
322         'lis_person_name_given' => [
323             'suffix' => '',
324             'group' => null,
325             'claim' => 'given_name',
326             'isarray' => false
327         ],
328         'lis_person_sourcedid' => [
329             'suffix' => '',
330             'group' => 'lis',
331             'claim' => 'person_sourcedid',
332             'isarray' => false
333         ],
334         'user_id' => [
335             'suffix' => '',
336             'group' => null,
337             'claim' => 'sub',
338             'isarray' => false
339         ],
340         'user_image' => [
341             'suffix' => '',
342             'group' => null,
343             'claim' => 'picture',
344             'isarray' => false
345         ],
346         'roles' => [
347             'suffix' => '',
348             'group' => '',
349             'claim' => 'roles',
350             'isarray' => true
351         ],
352         'role_scope_mentor' => [
353             'suffix' => '',
354             'group' => '',
355             'claim' => 'role_scope_mentor',
356             'isarray' => false
357         ],
358         'deployment_id' => [
359             'suffix' => '',
360             'group' => '',
361             'claim' => 'deployment_id',
362             'isarray' => false
363         ],
364         'lti_message_type' => [
365             'suffix' => '',
366             'group' => '',
367             'claim' => 'message_type',
368             'isarray' => false
369         ],
370         'lti_version' => [
371             'suffix' => '',
372             'group' => '',
373             'claim' => 'version',
374             'isarray' => false
375         ],
376         'resource_link_description' => [
377             'suffix' => '',
378             'group' => 'resource_link',
379             'claim' => 'description',
380             'isarray' => false
381         ],
382         'resource_link_id' => [
383             'suffix' => '',
384             'group' => 'resource_link',
385             'claim' => 'id',
386             'isarray' => false
387         ],
388         'resource_link_title' => [
389             'suffix' => '',
390             'group' => 'resource_link',
391             'claim' => 'title',
392             'isarray' => false
393         ],
394         'tool_consumer_info_product_family_code' => [
395             'suffix' => '',
396             'group' => 'tool_platform',
397             'claim' => 'product_family_code',
398             'isarray' => false
399         ],
400         'tool_consumer_info_version' => [
401             'suffix' => '',
402             'group' => 'tool_platform',
403             'claim' => 'version',
404             'isarray' => false
405         ],
406         'tool_consumer_instance_contact_email' => [
407             'suffix' => '',
408             'group' => 'tool_platform',
409             'claim' => 'contact_email',
410             'isarray' => false
411         ],
412         'tool_consumer_instance_description' => [
413             'suffix' => '',
414             'group' => 'tool_platform',
415             'claim' => 'description',
416             'isarray' => false
417         ],
418         'tool_consumer_instance_guid' => [
419             'suffix' => '',
420             'group' => 'tool_platform',
421             'claim' => 'guid',
422             'isarray' => false
423         ],
424         'tool_consumer_instance_name' => [
425             'suffix' => '',
426             'group' => 'tool_platform',
427             'claim' => 'name',
428             'isarray' => false
429         ],
430         'tool_consumer_instance_url' => [
431             'suffix' => '',
432             'group' => 'tool_platform',
433             'claim' => 'url',
434             'isarray' => false
435         ],
436         'custom_context_memberships_url' => [
437             'suffix' => 'nrps',
438             'group' => 'namesroleservice',
439             'claim' => 'context_memberships_url',
440             'isarray' => false
441         ],
442         'custom_context_memberships_versions' => [
443             'suffix' => 'nrps',
444             'group' => 'namesroleservice',
445             'claim' => 'service_versions',
446             'isarray' => true
447         ],
448         'custom_gradebookservices_scope' => [
449             'suffix' => 'ags',
450             'group' => 'endpoint',
451             'claim' => 'scope',
452             'isarray' => true
453         ],
454         'custom_lineitems_url' => [
455             'suffix' => 'ags',
456             'group' => 'endpoint',
457             'claim' => 'lineitems',
458             'isarray' => false
459         ],
460         'custom_lineitem_url' => [
461             'suffix' => 'ags',
462             'group' => 'endpoint',
463             'claim' => 'lineitem',
464             'isarray' => false
465         ],
466         'custom_results_url' => [
467             'suffix' => 'ags',
468             'group' => 'endpoint',
469             'claim' => 'results',
470             'isarray' => false
471         ],
472         'custom_result_url' => [
473             'suffix' => 'ags',
474             'group' => 'endpoint',
475             'claim' => 'result',
476             'isarray' => false
477         ],
478         'custom_scores_url' => [
479             'suffix' => 'ags',
480             'group' => 'endpoint',
481             'claim' => 'scores',
482             'isarray' => false
483         ],
484         'custom_score_url' => [
485             'suffix' => 'ags',
486             'group' => 'endpoint',
487             'claim' => 'score',
488             'isarray' => false
489         ],
490         'lis_outcome_service_url' => [
491             'suffix' => 'bo',
492             'group' => 'basicoutcome',
493             'claim' => 'lis_outcome_service_url',
494             'isarray' => false
495         ],
496         'lis_result_sourcedid' => [
497             'suffix' => 'bo',
498             'group' => 'basicoutcome',
499             'claim' => 'lis_result_sourcedid',
500             'isarray' => false
501         ],
502     );
505 /**
506  * Return the type of the instance, using domain matching if no explicit type is set.
507  *
508  * @param  object $instance the external tool activity settings
509  * @return object|null
510  * @since  Moodle 3.9
511  */
512 function lti_get_instance_type(object $instance) : ?object {
513     if (empty($instance->typeid)) {
514         if (!$tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course)) {
515             $tool = lti_get_tool_by_url_match($instance->securetoolurl,  $instance->course);
516         }
517         return $tool;
518     }
519     return lti_get_type($instance->typeid);
522 /**
523  * Return the launch data required for opening the external tool.
524  *
525  * @param  stdClass $instance the external tool activity settings
526  * @param  string $nonce  the nonce value to use (applies to LTI 1.3 only)
527  * @return array the endpoint URL and parameters (including the signature)
528  * @since  Moodle 3.0
529  */
530 function lti_get_launch_data($instance, $nonce = '') {
531     global $PAGE, $CFG, $USER;
533     $tool = lti_get_instance_type($instance);
534     if ($tool) {
535         $typeid = $tool->id;
536         $ltiversion = $tool->ltiversion;
537     } else {
538         $typeid = null;
539         $ltiversion = LTI_VERSION_1;
540     }
542     if ($typeid) {
543         $typeconfig = lti_get_type_config($typeid);
544     } else {
545         // There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults.
546         $typeconfig = (array)$instance;
548         $typeconfig['sendname'] = $instance->instructorchoicesendname;
549         $typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr;
550         $typeconfig['customparameters'] = $instance->instructorcustomparameters;
551         $typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades;
552         $typeconfig['allowroster'] = $instance->instructorchoiceallowroster;
553         $typeconfig['forcessl'] = '0';
554     }
556     if (isset($tool->toolproxyid)) {
557         $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
558         $key = $toolproxy->guid;
559         $secret = $toolproxy->secret;
560     } else {
561         $toolproxy = null;
562         if (!empty($instance->resourcekey)) {
563             $key = $instance->resourcekey;
564         } else if ($ltiversion === LTI_VERSION_1P3) {
565             $key = $tool->clientid;
566         } else if (!empty($typeconfig['resourcekey'])) {
567             $key = $typeconfig['resourcekey'];
568         } else {
569             $key = '';
570         }
571         if (!empty($instance->password)) {
572             $secret = $instance->password;
573         } else if (!empty($typeconfig['password'])) {
574             $secret = $typeconfig['password'];
575         } else {
576             $secret = '';
577         }
578     }
580     $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl'];
581     $endpoint = trim($endpoint);
583     // If the current request is using SSL and a secure tool URL is specified, use it.
584     if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) {
585         $endpoint = trim($instance->securetoolurl);
586     }
588     // If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL.
589     if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
590         if (!empty($instance->securetoolurl)) {
591             $endpoint = trim($instance->securetoolurl);
592         }
594         $endpoint = lti_ensure_url_is_https($endpoint);
595     } else {
596         if (!strstr($endpoint, '://')) {
597             $endpoint = 'http://' . $endpoint;
598         }
599     }
601     $orgid = lti_get_organizationid($typeconfig);
603     $course = $PAGE->course;
604     $islti2 = isset($tool->toolproxyid);
605     $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2);
606     if ($islti2) {
607         $requestparams = lti_build_request_lti2($tool, $allparams);
608     } else {
609         $requestparams = $allparams;
610     }
611     $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion));
612     $customstr = '';
613     if (isset($typeconfig['customparameters'])) {
614         $customstr = $typeconfig['customparameters'];
615     }
616     $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr,
617         $instance->instructorcustomparameters, $islti2));
619     $launchcontainer = lti_get_launch_container($instance, $typeconfig);
620     $returnurlparams = array('course' => $course->id,
621         'launch_container' => $launchcontainer,
622         'instanceid' => $instance->id,
623         'sesskey' => sesskey());
625     // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns.
626     $url = new \moodle_url('/mod/lti/return.php', $returnurlparams);
627     $returnurl = $url->out(false);
629     if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
630         $returnurl = lti_ensure_url_is_https($returnurl);
631     }
633     $target = '';
634     switch($launchcontainer) {
635         case LTI_LAUNCH_CONTAINER_EMBED:
636         case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS:
637             $target = 'iframe';
638             break;
639         case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW:
640             $target = 'frame';
641             break;
642         case LTI_LAUNCH_CONTAINER_WINDOW:
643             $target = 'window';
644             break;
645     }
646     if (!empty($target)) {
647         $requestparams['launch_presentation_document_target'] = $target;
648     }
650     $requestparams['launch_presentation_return_url'] = $returnurl;
652     // Add the parameters configured by the LTI services.
653     if ($typeid && !$islti2) {
654         $services = lti_get_services();
655         foreach ($services as $service) {
656             $serviceparameters = $service->get_launch_parameters('basic-lti-launch-request',
657                     $course->id, $USER->id , $typeid, $instance->id);
658             foreach ($serviceparameters as $paramkey => $paramvalue) {
659                 $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
660                     $islti2);
661             }
662         }
663     }
665     // Allow request params to be updated by sub-plugins.
666     $plugins = core_component::get_plugin_list('ltisource');
667     foreach (array_keys($plugins) as $plugin) {
668         $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch',
669             array($instance, $endpoint, $requestparams), array());
671         if (!empty($pluginparams) && is_array($pluginparams)) {
672             $requestparams = array_merge($requestparams, $pluginparams);
673         }
674     }
676     if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) {
677         if ($ltiversion !== LTI_VERSION_1P3) {
678             $parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret);
679         } else {
680             $parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce);
681         }
683         $endpointurl = new \moodle_url($endpoint);
684         $endpointparams = $endpointurl->params();
686         // Strip querystring params in endpoint url from $parms to avoid duplication.
687         if (!empty($endpointparams) && !empty($parms)) {
688             foreach (array_keys($endpointparams) as $paramname) {
689                 if (isset($parms[$paramname])) {
690                     unset($parms[$paramname]);
691                 }
692             }
693         }
695     } else {
696         // If no key and secret, do the launch unsigned.
697         $returnurlparams['unsigned'] = '1';
698         $parms = $requestparams;
699     }
701     return array($endpoint, $parms);
704 /**
705  * Launch an external tool activity.
706  *
707  * @param  stdClass $instance the external tool activity settings
708  * @return string The HTML code containing the javascript code for the launch
709  */
710 function lti_launch_tool($instance) {
712     list($endpoint, $parms) = lti_get_launch_data($instance);
713     $debuglaunch = ( $instance->debuglaunch == 1 );
715     $content = lti_post_launch_html($parms, $endpoint, $debuglaunch);
717     echo $content;
720 /**
721  * Prepares an LTI registration request message
722  *
723  * @param object $toolproxy  Tool Proxy instance object
724  */
725 function lti_register($toolproxy) {
726     $endpoint = $toolproxy->regurl;
728     // Change the status to pending.
729     $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING;
730     lti_update_tool_proxy($toolproxy);
732     $requestparams = lti_build_registration_request($toolproxy);
734     $content = lti_post_launch_html($requestparams, $endpoint, false);
736     echo $content;
740 /**
741  * Gets the parameters for the regirstration request
742  *
743  * @param object $toolproxy Tool Proxy instance object
744  * @return array Registration request parameters
745  */
746 function lti_build_registration_request($toolproxy) {
747     $key = $toolproxy->guid;
748     $secret = $toolproxy->secret;
750     $requestparams = array();
751     $requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest';
752     $requestparams['lti_version'] = 'LTI-2p0';
753     $requestparams['reg_key'] = $key;
754     $requestparams['reg_password'] = $secret;
755     $requestparams['reg_url'] = $toolproxy->regurl;
757     // Add the profile URL.
758     $profileservice = lti_get_service_by_name('profile');
759     $profileservice->set_tool_proxy($toolproxy);
760     $requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url');
762     // Add the return URL.
763     $returnurlparams = array('id' => $toolproxy->id, 'sesskey' => sesskey());
764     $url = new \moodle_url('/mod/lti/externalregistrationreturn.php', $returnurlparams);
765     $returnurl = $url->out(false);
767     $requestparams['launch_presentation_return_url'] = $returnurl;
769     return $requestparams;
773 /** get Organization ID using default if no value provided
774  * @param object $typeconfig
775  * @return string
776  */
777 function lti_get_organizationid($typeconfig) {
778     global $CFG;
779     // Default the organizationid if not specified.
780     if (empty($typeconfig['organizationid'])) {
781         if (($typeconfig['organizationid_default'] ?? LTI_DEFAULT_ORGID_SITEHOST) == LTI_DEFAULT_ORGID_SITEHOST) {
782             $urlparts = parse_url($CFG->wwwroot);
783             return $urlparts['host'];
784         } else {
785             return md5(get_site_identifier());
786         }
787     }
788     return $typeconfig['organizationid'];
791 /**
792  * Build source ID
793  *
794  * @param int $instanceid
795  * @param int $userid
796  * @param string $servicesalt
797  * @param null|int $typeid
798  * @param null|int $launchid
799  * @return stdClass
800  */
801 function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) {
802     $data = new \stdClass();
804     $data->instanceid = $instanceid;
805     $data->userid = $userid;
806     $data->typeid = $typeid;
807     if (!empty($launchid)) {
808         $data->launchid = $launchid;
809     } else {
810         $data->launchid = mt_rand();
811     }
813     $json = json_encode($data);
815     $hash = hash('sha256', $json . $servicesalt, false);
817     $container = new \stdClass();
818     $container->data = $data;
819     $container->hash = $hash;
821     return $container;
824 /**
825  * This function builds the request that must be sent to the tool producer
826  *
827  * @param object    $instance       Basic LTI instance object
828  * @param array     $typeconfig     Basic LTI tool configuration
829  * @param object    $course         Course object
830  * @param int|null  $typeid         Basic LTI tool ID
831  * @param boolean   $islti2         True if an LTI 2 tool is being launched
832  *
833  * @return array                    Request details
834  */
835 function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false) {
836     global $USER, $CFG;
838     if (empty($instance->cmid)) {
839         $instance->cmid = 0;
840     }
842     $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2);
844     $requestparams = array(
845         'user_id' => $USER->id,
846         'lis_person_sourcedid' => $USER->idnumber,
847         'roles' => $role,
848         'context_id' => $course->id,
849         'context_label' => trim(html_to_text($course->shortname, 0)),
850         'context_title' => trim(html_to_text($course->fullname, 0)),
851     );
852     if (!empty($instance->name)) {
853         $requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0));
854     }
855     if (!empty($instance->cmid)) {
856         $intro = format_module_intro('lti', $instance, $instance->cmid);
857         $intro = trim(html_to_text($intro, 0, false));
859         // This may look weird, but this is required for new lines
860         // so we generate the same OAuth signature as the tool provider.
861         $intro = str_replace("\n", "\r\n", $intro);
862         $requestparams['resource_link_description'] = $intro;
863     }
864     if (!empty($instance->id)) {
865         $requestparams['resource_link_id'] = $instance->id;
866     }
867     if (!empty($instance->resource_link_id)) {
868         $requestparams['resource_link_id'] = $instance->resource_link_id;
869     }
870     if ($course->format == 'site') {
871         $requestparams['context_type'] = 'Group';
872     } else {
873         $requestparams['context_type'] = 'CourseSection';
874         $requestparams['lis_course_section_sourcedid'] = $course->idnumber;
875     }
877     if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 ||
878             $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS ||
879             ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))
880     ) {
881         $placementsecret = $instance->servicesalt;
882         $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid));
883         $requestparams['lis_result_sourcedid'] = $sourcedid;
885         // Add outcome service URL.
886         $serviceurl = new \moodle_url('/mod/lti/service.php');
887         $serviceurl = $serviceurl->out();
889         $forcessl = false;
890         if (!empty($CFG->mod_lti_forcessl)) {
891             $forcessl = true;
892         }
894         if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) {
895             $serviceurl = lti_ensure_url_is_https($serviceurl);
896         }
898         $requestparams['lis_outcome_service_url'] = $serviceurl;
899     }
901     // Send user's name and email data if appropriate.
902     if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS ||
903         ($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname)
904             && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS)
905     ) {
906         $requestparams['lis_person_name_given'] = $USER->firstname;
907         $requestparams['lis_person_name_family'] = $USER->lastname;
908         $requestparams['lis_person_name_full'] = fullname($USER);
909         $requestparams['ext_user_username'] = $USER->username;
910     }
912     if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS ||
913         ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr)
914             && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS)
915     ) {
916         $requestparams['lis_person_contact_email_primary'] = $USER->email;
917     }
919     return $requestparams;
922 /**
923  * This function builds the request that must be sent to an LTI 2 tool provider
924  *
925  * @param object    $tool           Basic LTI tool object
926  * @param array     $params         Custom launch parameters
927  *
928  * @return array                    Request details
929  */
930 function lti_build_request_lti2($tool, $params) {
932     $requestparams = array();
934     $capabilities = lti_get_capabilities();
935     $enabledcapabilities = explode("\n", $tool->enabledcapability);
936     foreach ($enabledcapabilities as $capability) {
937         if (array_key_exists($capability, $capabilities)) {
938             $val = $capabilities[$capability];
939             if ($val && (substr($val, 0, 1) != '$')) {
940                 if (isset($params[$val])) {
941                     $requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]];
942                 }
943             }
944         }
945     }
947     return $requestparams;
951 /**
952  * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer
953  *
954  * @param stdClass  $instance       Basic LTI instance object
955  * @param string    $orgid          Organisation ID
956  * @param boolean   $islti2         True if an LTI 2 tool is being launched
957  * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
958  *
959  * @return array                    Request details
960  * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
961  * @see lti_build_standard_message()
962  */
963 function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') {
964     if (!$islti2) {
965         $ltiversion = LTI_VERSION_1;
966     } else {
967         $ltiversion = LTI_VERSION_2;
968     }
969     return lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype);
972 /**
973  * This function builds the standard parameters for an LTI message that must be sent to the tool producer
974  *
975  * @param stdClass  $instance       Basic LTI instance object
976  * @param string    $orgid          Organisation ID
977  * @param boolean   $ltiversion     LTI version to be used for tool messages
978  * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
979  *
980  * @return array                    Message parameters
981  */
982 function lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype = 'basic-lti-launch-request') {
983     global $CFG;
985     $requestparams = array();
987     if ($instance) {
988         $requestparams['resource_link_id'] = $instance->id;
989         if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) {
990             $requestparams['resource_link_id'] = $instance->resource_link_id;
991         }
992     }
994     $requestparams['launch_presentation_locale'] = current_language();
996     // Make sure we let the tool know what LMS they are being called from.
997     $requestparams['ext_lms'] = 'moodle-2';
998     $requestparams['tool_consumer_info_product_family_code'] = 'moodle';
999     $requestparams['tool_consumer_info_version'] = strval($CFG->version);
1001     // Add oauth_callback to be compliant with the 1.0A spec.
1002     $requestparams['oauth_callback'] = 'about:blank';
1004     $requestparams['lti_version'] = $ltiversion;
1005     $requestparams['lti_message_type'] = $messagetype;
1007     if ($orgid) {
1008         $requestparams["tool_consumer_instance_guid"] = $orgid;
1009     }
1010     if (!empty($CFG->mod_lti_institution_name)) {
1011         $requestparams['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0));
1012     } else {
1013         $requestparams['tool_consumer_instance_name'] = get_site()->shortname;
1014     }
1015     $requestparams['tool_consumer_instance_description'] = trim(html_to_text(get_site()->fullname, 0));
1017     return $requestparams;
1020 /**
1021  * This function builds the custom parameters
1022  *
1023  * @param object    $toolproxy      Tool proxy instance object
1024  * @param object    $tool           Tool instance object
1025  * @param object    $instance       Tool placement instance object
1026  * @param array     $params         LTI launch parameters
1027  * @param string    $customstr      Custom parameters defined for tool
1028  * @param string    $instructorcustomstr      Custom parameters defined for this placement
1029  * @param boolean   $islti2         True if an LTI 2 tool is being launched
1030  *
1031  * @return array                    Custom parameters
1032  */
1033 function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) {
1035     // Concatenate the custom parameters from the administrator and the instructor
1036     // Instructor parameters are only taken into consideration if the administrator
1037     // has given permission.
1038     $custom = array();
1039     if ($customstr) {
1040         $custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2);
1041     }
1042     if ($instructorcustomstr) {
1043         $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
1044             $instructorcustomstr, $islti2), $custom);
1045     }
1046     if ($islti2) {
1047         $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
1048             $tool->parameter, true), $custom);
1049         $settings = lti_get_tool_settings($tool->toolproxyid);
1050         $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1051         if (!empty($instance->course)) {
1052             $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course);
1053             $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1054             if (!empty($instance->id)) {
1055                 $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id);
1056                 $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
1057             }
1058         }
1059     }
1061     return $custom;
1064 /**
1065  * Builds a standard LTI Content-Item selection request.
1066  *
1067  * @param int $id The tool type ID.
1068  * @param stdClass $course The course object.
1069  * @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP)
1070  *                              will use to return the Content-Item message.
1071  * @param string $title The tool's title, if available.
1072  * @param string $text The text to display to represent the content item. This value may be a long description of the content item.
1073  * @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default.
1074  * @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened
1075  *                                   (via the presentationDocumentTarget element for a returned content item).
1076  *                                   If empty, "frame", "iframe", and "window" will be supported by default.
1077  * @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without
1078  * @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default.
1079  *                         any option for the user to cancel the operation. False by default.
1080  * @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not.
1081  *                       A signed message should always be required when the content item is being created automatically in the
1082  *                       TC without further interaction from the user. False by default.
1083  * @param bool $canconfirm Flag for can_confirm parameter. False by default.
1084  * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default.
1085  * @param string $nonce
1086  * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface.
1087  * @throws moodle_exception When the LTI tool type does not exist.`
1088  * @throws coding_exception For invalid media type and presentation target parameters.
1089  */
1090 function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
1091                                                   $presentationtargets = [], $autocreate = false, $multiple = true,
1092                                                   $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {
1093     global $USER;
1095     $tool = lti_get_type($id);
1096     // Validate parameters.
1097     if (!$tool) {
1098         throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1099     }
1100     if (!is_array($mediatypes)) {
1101         throw new coding_exception('The list of accepted media types should be in an array');
1102     }
1103     if (!is_array($presentationtargets)) {
1104         throw new coding_exception('The list of accepted presentation targets should be in an array');
1105     }
1107     // Check title. If empty, use the tool's name.
1108     if (empty($title)) {
1109         $title = $tool->name;
1110     }
1112     $typeconfig = lti_get_type_config($id);
1113     $key = '';
1114     $secret = '';
1115     $islti2 = false;
1116     $islti13 = false;
1117     if (isset($tool->toolproxyid)) {
1118         $islti2 = true;
1119         $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
1120         $key = $toolproxy->guid;
1121         $secret = $toolproxy->secret;
1122     } else {
1123         $islti13 = $tool->ltiversion === LTI_VERSION_1P3;
1124         $toolproxy = null;
1125         if ($islti13 && !empty($tool->clientid)) {
1126             $key = $tool->clientid;
1127         } else if (!$islti13 && !empty($typeconfig['resourcekey'])) {
1128             $key = $typeconfig['resourcekey'];
1129         }
1130         if (!empty($typeconfig['password'])) {
1131             $secret = $typeconfig['password'];
1132         }
1133     }
1134     $tool->enabledcapability = '';
1135     if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) {
1136         $tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest'];
1137     }
1139     $tool->parameter = '';
1140     if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) {
1141         $tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest'];
1142     }
1144     // Set the tool URL.
1145     if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) {
1146         $toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']);
1147     } else {
1148         $toolurl = new moodle_url($typeconfig['toolurl']);
1149     }
1151     // Check if SSL is forced.
1152     if (!empty($typeconfig['forcessl'])) {
1153         // Make sure the tool URL is set to https.
1154         if (strtolower($toolurl->get_scheme()) === 'http') {
1155             $toolurl->set_scheme('https');
1156         }
1157         // Make sure the return URL is set to https.
1158         if (strtolower($returnurl->get_scheme()) === 'http') {
1159             $returnurl->set_scheme('https');
1160         }
1161     }
1162     $toolurlout = $toolurl->out(false);
1164     // Get base request parameters.
1165     $instance = new stdClass();
1166     $instance->course = $course->id;
1167     $requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2);
1169     // Get LTI2-specific request parameters and merge to the request parameters if applicable.
1170     if ($islti2) {
1171         $lti2params = lti_build_request_lti2($tool, $requestparams);
1172         $requestparams = array_merge($requestparams, $lti2params);
1173     }
1175     // Get standard request parameters and merge to the request parameters.
1176     $orgid = lti_get_organizationid($typeconfig);
1177     $standardparams = lti_build_standard_message(null, $orgid, $tool->ltiversion, 'ContentItemSelectionRequest');
1178     $requestparams = array_merge($requestparams, $standardparams);
1180     // Get custom request parameters and merge to the request parameters.
1181     $customstr = '';
1182     if (!empty($typeconfig['customparameters'])) {
1183         $customstr = $typeconfig['customparameters'];
1184     }
1185     $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2);
1186     $requestparams = array_merge($requestparams, $customparams);
1188     // Add the parameters configured by the LTI services.
1189     if ($id && !$islti2) {
1190         $services = lti_get_services();
1191         foreach ($services as $service) {
1192             $serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
1193                 $course->id, $USER->id , $id);
1194             foreach ($serviceparameters as $paramkey => $paramvalue) {
1195                 $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
1196                     $islti2);
1197             }
1198         }
1199     }
1201     // Allow request params to be updated by sub-plugins.
1202     $plugins = core_component::get_plugin_list('ltisource');
1203     foreach (array_keys($plugins) as $plugin) {
1204         $pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []);
1206         if (!empty($pluginparams) && is_array($pluginparams)) {
1207             $requestparams = array_merge($requestparams, $pluginparams);
1208         }
1209     }
1211     if (!$islti13) {
1212         // Media types. Set to ltilink by default if empty.
1213         if (empty($mediatypes)) {
1214             $mediatypes = [
1215                 'application/vnd.ims.lti.v1.ltilink',
1216             ];
1217         }
1218         $requestparams['accept_media_types'] = implode(',', $mediatypes);
1219     } else {
1220         // Only LTI links are currently supported.
1221         $requestparams['accept_types'] = 'ltiResourceLink';
1222     }
1224     // Presentation targets. Supports frame, iframe, window by default if empty.
1225     if (empty($presentationtargets)) {
1226         $presentationtargets = [
1227             'frame',
1228             'iframe',
1229             'window',
1230         ];
1231     }
1232     $requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets);
1234     // Other request parameters.
1235     $requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false';
1236     $requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false';
1237     $requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false';
1238     $requestparams['auto_create'] = $autocreate === true ? 'true' : 'false';
1239     $requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false';
1240     $requestparams['content_item_return_url'] = $returnurl->out(false);
1241     $requestparams['title'] = $title;
1242     $requestparams['text'] = $text;
1243     if (!$islti13) {
1244         $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret);
1245     } else {
1246         $signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce);
1247     }
1248     $toolurlparams = $toolurl->params();
1250     // Strip querystring params in endpoint url from $signedparams to avoid duplication.
1251     if (!empty($toolurlparams) && !empty($signedparams)) {
1252         foreach (array_keys($toolurlparams) as $paramname) {
1253             if (isset($signedparams[$paramname])) {
1254                 unset($signedparams[$paramname]);
1255             }
1256         }
1257     }
1259     // Check for params that should not be passed. Unset if they are set.
1260     $unwantedparams = [
1261         'resource_link_id',
1262         'resource_link_title',
1263         'resource_link_description',
1264         'launch_presentation_return_url',
1265         'lis_result_sourcedid',
1266     ];
1267     foreach ($unwantedparams as $param) {
1268         if (isset($signedparams[$param])) {
1269             unset($signedparams[$param]);
1270         }
1271     }
1273     // Prepare result object.
1274     $result = new stdClass();
1275     $result->params = $signedparams;
1276     $result->url = $toolurlout;
1278     return $result;
1281 /**
1282  * Verifies the OAuth signature of an incoming message.
1283  *
1284  * @param int $typeid The tool type ID.
1285  * @param string $consumerkey The consumer key.
1286  * @return stdClass Tool type
1287  * @throws moodle_exception
1288  * @throws lti\OAuthException
1289  */
1290 function lti_verify_oauth_signature($typeid, $consumerkey) {
1291     $tool = lti_get_type($typeid);
1292     // Validate parameters.
1293     if (!$tool) {
1294         throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1295     }
1296     $typeconfig = lti_get_type_config($typeid);
1298     if (isset($tool->toolproxyid)) {
1299         $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
1300         $key = $toolproxy->guid;
1301         $secret = $toolproxy->secret;
1302     } else {
1303         $toolproxy = null;
1304         if (!empty($typeconfig['resourcekey'])) {
1305             $key = $typeconfig['resourcekey'];
1306         } else {
1307             $key = '';
1308         }
1309         if (!empty($typeconfig['password'])) {
1310             $secret = $typeconfig['password'];
1311         } else {
1312             $secret = '';
1313         }
1314     }
1316     if ($consumerkey !== $key) {
1317         throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1318     }
1320     $store = new lti\TrivialOAuthDataStore();
1321     $store->add_consumer($key, $secret);
1322     $server = new lti\OAuthServer($store);
1323     $method = new lti\OAuthSignatureMethod_HMAC_SHA1();
1324     $server->add_signature_method($method);
1325     $request = lti\OAuthRequest::from_request();
1326     try {
1327         $server->verify_request($request);
1328     } catch (lti\OAuthException $e) {
1329         throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage());
1330     }
1332     return $tool;
1335 /**
1336  * Verifies the JWT signature using a JWK keyset.
1337  *
1338  * @param string $jwtparam JWT parameter value.
1339  * @param string $keyseturl The tool keyseturl.
1340  * @param string $clientid The tool client id.
1341  *
1342  * @return object The JWT's payload as a PHP object
1343  * @throws moodle_exception
1344  * @throws UnexpectedValueException     Provided JWT was invalid
1345  * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
1346  * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1347  * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
1348  * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
1349  */
1350 function lti_verify_with_keyset($jwtparam, $keyseturl, $clientid) {
1351     // Attempts to retrieve cached keyset.
1352     $cache = cache::make('mod_lti', 'keyset');
1353     $keyset = $cache->get($clientid);
1355     try {
1356         if (empty($keyset)) {
1357             throw new moodle_exception('errornocachedkeysetfound', 'mod_lti');
1358         }
1359         $keysetarr = json_decode($keyset, true);
1360         $keys = JWK::parseKeySet($keysetarr);
1361         $jwt = JWT::decode($jwtparam, $keys, ['RS256']);
1362     } catch (Exception $e) {
1363         // Something went wrong, so attempt to update cached keyset and then try again.
1364         $keyset = file_get_contents($keyseturl);
1365         $keysetarr = json_decode($keyset, true);
1366         $keys = JWK::parseKeySet($keysetarr);
1367         $jwt = JWT::decode($jwtparam, $keys, ['RS256']);
1368         // If sucessful, updates the cached keyset.
1369         $cache->set($clientid, $keyset);
1370     }
1371     return $jwt;
1374 /**
1375  * Verifies the JWT signature of an incoming message.
1376  *
1377  * @param int $typeid The tool type ID.
1378  * @param string $consumerkey The consumer key.
1379  * @param string $jwtparam JWT parameter value
1380  *
1381  * @return stdClass Tool type
1382  * @throws moodle_exception
1383  * @throws UnexpectedValueException     Provided JWT was invalid
1384  * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
1385  * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
1386  * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
1387  * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
1388  */
1389 function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
1390     $tool = lti_get_type($typeid);
1392     // Validate parameters.
1393     if (!$tool) {
1394         throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1395     }
1396     if (isset($tool->toolproxyid)) {
1397         throw new moodle_exception('JWT security not supported with LTI 2');
1398     }
1400     $typeconfig = lti_get_type_config($typeid);
1402     $key = $tool->clientid ?? '';
1404     if ($consumerkey !== $key) {
1405         throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
1406     }
1408     if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) {
1409         $publickey = $typeconfig['publickey'] ?? '';
1410         if (empty($publickey)) {
1411             throw new moodle_exception('No public key configured');
1412         }
1413         // Attemps to verify jwt with RSA key.
1414         JWT::decode($jwtparam, $publickey, ['RS256']);
1415     } else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) {
1416         $keyseturl = $typeconfig['publickeyset'] ?? '';
1417         if (empty($keyseturl)) {
1418             throw new moodle_exception('No public keyset configured');
1419         }
1420         // Attempts to verify jwt with jwk keyset.
1421         lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid);
1422     } else {
1423         throw new moodle_exception('Invalid public key type');
1424     }
1426     return $tool;
1429 /**
1430  * Converts LTI 1.1 Content Item for LTI Link to Form data.
1431  *
1432  * @param object $tool Tool for which the item is created for.
1433  * @param object $typeconfig The tool configuration.
1434  * @param object $item Item populated from JSON to be converted to Form form
1435  *
1436  * @return stdClass Form config for the item
1437  */
1438 function content_item_to_form(object $tool, object $typeconfig, object $item) : stdClass {
1439     $config = new stdClass();
1440     $config->name = '';
1441     if (isset($item->title)) {
1442         $config->name = $item->title;
1443     }
1444     if (empty($config->name)) {
1445         $config->name = $tool->name;
1446     }
1447     if (isset($item->text)) {
1448         $config->introeditor = [
1449             'text' => $item->text,
1450             'format' => FORMAT_PLAIN
1451         ];
1452     } else {
1453         $config->introeditor = [
1454             'text' => '',
1455             'format' => FORMAT_PLAIN
1456         ];
1457     }
1458     if (isset($item->icon->{'@id'})) {
1459         $iconurl = new moodle_url($item->icon->{'@id'});
1460         // Assign item's icon URL to secureicon or icon depending on its scheme.
1461         if (strtolower($iconurl->get_scheme()) === 'https') {
1462             $config->secureicon = $iconurl->out(false);
1463         } else {
1464             $config->icon = $iconurl->out(false);
1465         }
1466     }
1467     if (isset($item->url)) {
1468         $url = new moodle_url($item->url);
1469         $config->toolurl = $url->out(false);
1470         $config->typeid = 0;
1471     } else {
1472         $config->typeid = $tool->id;
1473     }
1474     $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
1475     $islti2 = $tool->ltiversion === LTI_VERSION_2;
1476     if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
1477         $acceptgrades = $typeconfig->lti_acceptgrades;
1478         if ($acceptgrades == LTI_SETTING_ALWAYS) {
1479             // We create a line item regardless if the definition contains one or not.
1480             $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1481             $config->grade_modgrade_point = 100;
1482         }
1483         if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
1484             if (isset($item->lineItem)) {
1485                 $lineitem = $item->lineItem;
1486                 $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
1487                 $maxscore = 100;
1488                 if (isset($lineitem->scoreConstraints)) {
1489                     $sc = $lineitem->scoreConstraints;
1490                     if (isset($sc->totalMaximum)) {
1491                         $maxscore = $sc->totalMaximum;
1492                     } else if (isset($sc->normalMaximum)) {
1493                         $maxscore = $sc->normalMaximum;
1494                     }
1495                 }
1496                 $config->grade_modgrade_point = $maxscore;
1497                 $config->lineitemresourceid = '';
1498                 $config->lineitemtag = '';
1499                 if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
1500                     $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';
1501                 }
1502                 if (isset($lineitem->tag)) {
1503                     $config->lineitemtag = $lineitem->tag?:'';
1504                 }
1505             }
1506         }
1507     }
1508     $config->instructorchoicesendname = LTI_SETTING_NEVER;
1509     $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
1510     $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
1511     if (isset($item->placementAdvice->presentationDocumentTarget)) {
1512         if ($item->placementAdvice->presentationDocumentTarget === 'window') {
1513             $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW;
1514         } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') {
1515             $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
1516         } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') {
1517             $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED;
1518         }
1519     }
1520     if (isset($item->custom)) {
1521         $customparameters = [];
1522         foreach ($item->custom as $key => $value) {
1523             $customparameters[] = "{$key}={$value}";
1524         }
1525         $config->instructorcustomparameters = implode("\n", $customparameters);
1526     }
1527     // Including a JSON version of the form data to support adding many items in one submit.
1528     $config->contentitemjson = json_encode($item);
1529     return $config;
1532 /**
1533  * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
1534  * selected content item. This configuration data can be then used when adding a tool into the course.
1535  *
1536  * @param int $typeid The tool type ID.
1537  * @param string $messagetype The value for the lti_message_type parameter.
1538  * @param string $ltiversion The value for the lti_version parameter.
1539  * @param string $consumerkey The consumer key.
1540  * @param string $contentitemsjson The JSON string for the content_items parameter.
1541  * @return stdClass The array of module information objects.
1542  * @throws moodle_exception
1543  * @throws lti\OAuthException
1544  */
1545 function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
1546     $tool = lti_get_type($typeid);
1547     // Validate parameters.
1548     if (!$tool) {
1549         throw new moodle_exception('errortooltypenotfound', 'mod_lti');
1550     }
1551     // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
1552     // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
1553     if ($messagetype !== 'ContentItemSelection') {
1554         debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
1555             DEBUG_DEVELOPER);
1556     }
1558     // Check LTI versions from our side and the response's side. Show debugging if they don't match.
1559     // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
1560     $expectedversion = $tool->ltiversion;
1561     $islti2 = ($expectedversion === LTI_VERSION_2);
1562     if ($ltiversion !== $expectedversion) {
1563         debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
1564             " Response: {$ltiversion}", DEBUG_DEVELOPER);
1565     }
1567     $items = json_decode($contentitemsjson);
1568     if (empty($items)) {
1569         throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
1570     }
1571     if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {
1572         throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
1573     }
1575     $config = null;
1576     $items = $items->{'@graph'};
1577     if (!empty($items)) {
1578         $typeconfig = lti_get_type_type_config($tool->id);
1579         if (count($items) == 1) {
1580             $config = content_item_to_form($tool, $typeconfig, $items[0]);
1581         } else {
1582             $multiple = [];
1583             foreach ($items as $item) {
1584                 $multiple[] = content_item_to_form($tool, $typeconfig, $item);
1585             }
1586             $config = new stdClass();
1587             $config->multiple = $multiple;
1588         }
1589     }
1590     return $config;
1593 /**
1594  * Converts the new Deep-Linking format for Content-Items to the old format.
1595  *
1596  * @param string $param JSON string representing new Deep-Linking format
1597  * @return string  JSON representation of content-items
1598  */
1599 function lti_convert_content_items($param) {
1600     $items = array();
1601     $json = json_decode($param);
1602     if (!empty($json) && is_array($json)) {
1603         foreach ($json as $item) {
1604             if (isset($item->type)) {
1605                 $newitem = clone $item;
1606                 switch ($item->type) {
1607                     case 'ltiResourceLink':
1608                         $newitem->{'@type'} = 'LtiLinkItem';
1609                         $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
1610                         break;
1611                     case 'link':
1612                     case 'rich':
1613                         $newitem->{'@type'} = 'ContentItem';
1614                         $newitem->mediaType = 'text/html';
1615                         break;
1616                     case 'file':
1617                         $newitem->{'@type'} = 'FileItem';
1618                         break;
1619                 }
1620                 unset($newitem->type);
1621                 if (isset($item->html)) {
1622                     $newitem->text = $item->html;
1623                     unset($newitem->html);
1624                 }
1625                 if (isset($item->iframe)) {
1626                     // DeepLinking allows multiple options to be declared as supported.
1627                     // We favor iframe over new window if both are specified.
1628                     $newitem->placementAdvice = new stdClass();
1629                     $newitem->placementAdvice->presentationDocumentTarget = 'iframe';
1630                     if (isset($item->iframe->width)) {
1631                         $newitem->placementAdvice->displayWidth = $item->iframe->width;
1632                     }
1633                     if (isset($item->iframe->height)) {
1634                         $newitem->placementAdvice->displayHeight = $item->iframe->height;
1635                     }
1636                     unset($newitem->iframe);
1637                     unset($newitem->window);
1638                 } else if (isset($item->window)) {
1639                     $newitem->placementAdvice = new stdClass();
1640                     $newitem->placementAdvice->presentationDocumentTarget = 'window';
1641                     if (isset($item->window->targetName)) {
1642                         $newitem->placementAdvice->windowTarget = $item->window->targetName;
1643                     }
1644                     if (isset($item->window->width)) {
1645                         $newitem->placementAdvice->displayWidth = $item->window->width;
1646                     }
1647                     if (isset($item->window->height)) {
1648                         $newitem->placementAdvice->displayHeight = $item->window->height;
1649                     }
1650                     unset($newitem->window);
1651                 } else if (isset($item->presentation)) {
1652                     // This may have been part of an early draft but is not in the final spec
1653                     // so keeping it around for now in case it's actually been used.
1654                     $newitem->placementAdvice = new stdClass();
1655                     if (isset($item->presentation->documentTarget)) {
1656                         $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
1657                     }
1658                     if (isset($item->presentation->windowTarget)) {
1659                         $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget;
1660                     }
1661                     if (isset($item->presentation->width)) {
1662                         $newitem->placementAdvice->dislayWidth = $item->presentation->width;
1663                     }
1664                     if (isset($item->presentation->height)) {
1665                         $newitem->placementAdvice->dislayHeight = $item->presentation->height;
1666                     }
1667                     unset($newitem->presentation);
1668                 }
1669                 if (isset($item->icon) && isset($item->icon->url)) {
1670                     $newitem->icon->{'@id'} = $item->icon->url;
1671                     unset($newitem->icon->url);
1672                 }
1673                 if (isset($item->thumbnail) && isset($item->thumbnail->url)) {
1674                     $newitem->thumbnail->{'@id'} = $item->thumbnail->url;
1675                     unset($newitem->thumbnail->url);
1676                 }
1677                 if (isset($item->lineItem)) {
1678                     unset($newitem->lineItem);
1679                     $newitem->lineItem = new stdClass();
1680                     $newitem->lineItem->{'@type'} = 'LineItem';
1681                     $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore';
1682                     if (isset($item->lineItem->label)) {
1683                         $newitem->lineItem->label = $item->lineItem->label;
1684                     }
1685                     if (isset($item->lineItem->resourceId)) {
1686                         $newitem->lineItem->assignedActivity = new stdClass();
1687                         $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId;
1688                     }
1689                     if (isset($item->lineItem->tag)) {
1690                         $newitem->lineItem->tag = $item->lineItem->tag;
1691                     }
1692                     if (isset($item->lineItem->scoreMaximum)) {
1693                         $newitem->lineItem->scoreConstraints = new stdClass();
1694                         $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits';
1695                         $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum;
1696                     }
1697                 }
1698                 $items[] = $newitem;
1699             }
1700         }
1701     }
1703     $newitems = new stdClass();
1704     $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
1705     $newitems->{'@graph'} = $items;
1707     return json_encode($newitems);
1710 function lti_get_tool_table($tools, $id) {
1711     global $OUTPUT;
1712     $html = '';
1714     $typename = get_string('typename', 'lti');
1715     $baseurl = get_string('baseurl', 'lti');
1716     $action = get_string('action', 'lti');
1717     $createdon = get_string('createdon', 'lti');
1719     if (!empty($tools)) {
1720         $html .= "
1721         <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\">
1722             <table id=\"{$id}_tools\">
1723                 <thead>
1724                     <tr>
1725                         <th>$typename</th>
1726                         <th>$baseurl</th>
1727                         <th>$createdon</th>
1728                         <th>$action</th>
1729                     </tr>
1730                 </thead>
1731         ";
1733         foreach ($tools as $type) {
1734             $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1735             $accept = get_string('accept', 'lti');
1736             $update = get_string('update', 'lti');
1737             $delete = get_string('delete', 'lti');
1739             if (empty($type->toolproxyid)) {
1740                 $baseurl = new \moodle_url('/mod/lti/typessettings.php', array(
1741                         'action' => 'accept',
1742                         'id' => $type->id,
1743                         'sesskey' => sesskey(),
1744                         'tab' => $id
1745                     ));
1746                 $ref = $type->baseurl;
1747             } else {
1748                 $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array(
1749                         'action' => 'accept',
1750                         'id' => $type->id,
1751                         'sesskey' => sesskey(),
1752                         'tab' => $id
1753                     ));
1754                 $ref = $type->tpname;
1755             }
1757             $accepthtml = $OUTPUT->action_icon($baseurl,
1758                     new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1759                     array('title' => $accept, 'class' => 'editing_accept'));
1761             $deleteaction = 'delete';
1763             if ($type->state == LTI_TOOL_STATE_CONFIGURED) {
1764                 $accepthtml = '';
1765             }
1767             if ($type->state != LTI_TOOL_STATE_REJECTED) {
1768                 $deleteaction = 'reject';
1769                 $delete = get_string('reject', 'lti');
1770             }
1772             $updateurl = clone($baseurl);
1773             $updateurl->param('action', 'update');
1774             $updatehtml = $OUTPUT->action_icon($updateurl,
1775                     new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1776                     array('title' => $update, 'class' => 'editing_update'));
1778             if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) {
1779                 $deleteurl = clone($baseurl);
1780                 $deleteurl->param('action', $deleteaction);
1781                 $deletehtml = $OUTPUT->action_icon($deleteurl,
1782                         new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1783                         array('title' => $delete, 'class' => 'editing_delete'));
1784             } else {
1785                 $deletehtml = '';
1786             }
1787             $html .= "
1788             <tr>
1789                 <td>
1790                     {$type->name}
1791                 </td>
1792                 <td>
1793                     {$ref}
1794                 </td>
1795                 <td>
1796                     {$date}
1797                 </td>
1798                 <td align=\"center\">
1799                     {$accepthtml}{$updatehtml}{$deletehtml}
1800                 </td>
1801             </tr>
1802             ";
1803         }
1804         $html .= '</table></div>';
1805     } else {
1806         $html .= get_string('no_' . $id, 'lti');
1807     }
1809     return $html;
1812 /**
1813  * This function builds the tab for a category of tool proxies
1814  *
1815  * @param object    $toolproxies    Tool proxy instance objects
1816  * @param string    $id             Category ID
1817  *
1818  * @return string                   HTML for tab
1819  */
1820 function lti_get_tool_proxy_table($toolproxies, $id) {
1821     global $OUTPUT;
1823     if (!empty($toolproxies)) {
1824         $typename = get_string('typename', 'lti');
1825         $url = get_string('registrationurl', 'lti');
1826         $action = get_string('action', 'lti');
1827         $createdon = get_string('createdon', 'lti');
1829         $html = <<< EOD
1830         <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em">
1831             <table id="{$id}_tool_proxies">
1832                 <thead>
1833                     <tr>
1834                         <th>{$typename}</th>
1835                         <th>{$url}</th>
1836                         <th>{$createdon}</th>
1837                         <th>{$action}</th>
1838                     </tr>
1839                 </thead>
1840 EOD;
1841         foreach ($toolproxies as $toolproxy) {
1842             $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1843             $accept = get_string('register', 'lti');
1844             $update = get_string('update', 'lti');
1845             $delete = get_string('delete', 'lti');
1847             $baseurl = new \moodle_url('/mod/lti/registersettings.php', array(
1848                     'action' => 'accept',
1849                     'id' => $toolproxy->id,
1850                     'sesskey' => sesskey(),
1851                     'tab' => $id
1852                 ));
1854             $registerurl = new \moodle_url('/mod/lti/register.php', array(
1855                     'id' => $toolproxy->id,
1856                     'sesskey' => sesskey(),
1857                     'tab' => 'tool_proxy'
1858                 ));
1860             $accepthtml = $OUTPUT->action_icon($registerurl,
1861                     new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1862                     array('title' => $accept, 'class' => 'editing_accept'));
1864             $deleteaction = 'delete';
1866             if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) {
1867                 $accepthtml = '';
1868             }
1870             if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) {
1871                 $delete = get_string('cancel', 'lti');
1872             }
1874             $updateurl = clone($baseurl);
1875             $updateurl->param('action', 'update');
1876             $updatehtml = $OUTPUT->action_icon($updateurl,
1877                     new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1878                     array('title' => $update, 'class' => 'editing_update'));
1880             $deleteurl = clone($baseurl);
1881             $deleteurl->param('action', $deleteaction);
1882             $deletehtml = $OUTPUT->action_icon($deleteurl,
1883                     new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1884                     array('title' => $delete, 'class' => 'editing_delete'));
1885             $html .= <<< EOD
1886             <tr>
1887                 <td>
1888                     {$toolproxy->name}
1889                 </td>
1890                 <td>
1891                     {$toolproxy->regurl}
1892                 </td>
1893                 <td>
1894                     {$date}
1895                 </td>
1896                 <td align="center">
1897                     {$accepthtml}{$updatehtml}{$deletehtml}
1898                 </td>
1899             </tr>
1900 EOD;
1901         }
1902         $html .= '</table></div>';
1903     } else {
1904         $html = get_string('no_' . $id, 'lti');
1905     }
1907     return $html;
1910 /**
1911  * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter
1912  *
1913  * @param object $tool  Tool instance object
1914  *
1915  * @return array List of enabled capabilities
1916  */
1917 function lti_get_enabled_capabilities($tool) {
1918     if (!isset($tool)) {
1919         return array();
1920     }
1921     if (!empty($tool->enabledcapability)) {
1922         $enabledcapabilities = explode("\n", $tool->enabledcapability);
1923     } else {
1924         $enabledcapabilities = array();
1925     }
1926     if (!empty($tool->parameter)) {
1927         $paramstr = str_replace("\r\n", "\n", $tool->parameter);
1928         $paramstr = str_replace("\n\r", "\n", $paramstr);
1929         $paramstr = str_replace("\r", "\n", $paramstr);
1930         $params = explode("\n", $paramstr);
1931         foreach ($params as $param) {
1932             $pos = strpos($param, '=');
1933             if (($pos === false) || ($pos < 1)) {
1934                 continue;
1935             }
1936             $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
1937             if (substr($value, 0, 1) == '$') {
1938                 $value = substr($value, 1);
1939                 if (!in_array($value, $enabledcapabilities)) {
1940                     $enabledcapabilities[] = $value;
1941                 }
1942             }
1943         }
1944     }
1945     return $enabledcapabilities;
1948 /**
1949  * Splits the custom parameters field to the various parameters
1950  *
1951  * @param object    $toolproxy      Tool proxy instance object
1952  * @param object    $tool           Tool instance object
1953  * @param array     $params         LTI launch parameters
1954  * @param string    $customstr      String containing the parameters
1955  * @param boolean   $islti2         True if an LTI 2 tool is being launched
1956  *
1957  * @return array of custom parameters
1958  */
1959 function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {
1960     $customstr = str_replace("\r\n", "\n", $customstr);
1961     $customstr = str_replace("\n\r", "\n", $customstr);
1962     $customstr = str_replace("\r", "\n", $customstr);
1963     $lines = explode("\n", $customstr);  // Or should this split on "/[\n;]/"?
1964     $retval = array();
1965     foreach ($lines as $line) {
1966         $pos = strpos($line, '=');
1967         if ( $pos === false || $pos < 1 ) {
1968             continue;
1969         }
1970         $key = trim(core_text::substr($line, 0, $pos));
1971         $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
1972         $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
1973         $key2 = lti_map_keyname($key);
1974         $retval['custom_'.$key2] = $val;
1975         if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) {
1976             $retval['custom_'.$key] = $val;
1977         }
1978     }
1979     return $retval;
1982 /**
1983  * Adds the custom parameters to an array
1984  *
1985  * @param object    $toolproxy      Tool proxy instance object
1986  * @param object    $tool           Tool instance object
1987  * @param array     $params         LTI launch parameters
1988  * @param array     $parameters     Array containing the parameters
1989  *
1990  * @return array    Array of custom parameters
1991  */
1992 function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {
1993     $retval = array();
1994     foreach ($parameters as $key => $val) {
1995         $key2 = lti_map_keyname($key);
1996         $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true);
1997         $retval['custom_'.$key2] = $val;
1998         if ($key != $key2) {
1999             $retval['custom_'.$key] = $val;
2000         }
2001     }
2002     return $retval;
2005 /**
2006  * Parse a custom parameter to replace any substitution variables
2007  *
2008  * @param object    $toolproxy      Tool proxy instance object
2009  * @param object    $tool           Tool instance object
2010  * @param array     $params         LTI launch parameters
2011  * @param string    $value          Custom parameter value
2012  * @param boolean   $islti2         True if an LTI 2 tool is being launched
2013  *
2014  * @return string Parsed value of custom parameter
2015  */
2016 function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {
2017     // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var.
2018     global $USER, $COURSE;
2020     if ($value) {
2021         if (substr($value, 0, 1) == '\\') {
2022             $value = substr($value, 1);
2023         } else if (substr($value, 0, 1) == '$') {
2024             $value1 = substr($value, 1);
2025             $enabledcapabilities = lti_get_enabled_capabilities($tool);
2026             if (!$islti2 || in_array($value1, $enabledcapabilities)) {
2027                 $capabilities = lti_get_capabilities();
2028                 if (array_key_exists($value1, $capabilities)) {
2029                     $val = $capabilities[$value1];
2030                     if ($val) {
2031                         if (substr($val, 0, 1) != '$') {
2032                             $value = $params[$val];
2033                         } else {
2034                             $valarr = explode('->', substr($val, 1), 2);
2035                             $value = "{${$valarr[0]}->{$valarr[1]}}";
2036                             $value = str_replace('<br />' , ' ', $value);
2037                             $value = str_replace('<br>' , ' ', $value);
2038                             $value = format_string($value);
2039                         }
2040                     } else {
2041                         $value = lti_calculate_custom_parameter($value1);
2042                     }
2043                 } else {
2044                     $val = $value;
2045                     $services = lti_get_services();
2046                     foreach ($services as $service) {
2047                         $service->set_tool_proxy($toolproxy);
2048                         $service->set_type($tool);
2049                         $value = $service->parse_value($val);
2050                         if ($val != $value) {
2051                             break;
2052                         }
2053                     }
2054                 }
2055             }
2056         }
2057     }
2058     return $value;
2061 /**
2062  * Calculates the value of a custom parameter that has not been specified earlier
2063  *
2064  * @param string    $value          Custom parameter value
2065  *
2066  * @return string Calculated value of custom parameter
2067  */
2068 function lti_calculate_custom_parameter($value) {
2069     global $USER, $COURSE;
2071     switch ($value) {
2072         case 'Moodle.Person.userGroupIds':
2073             return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]);
2074         case 'Context.id.history':
2075             return implode(",", get_course_history($COURSE));
2076     }
2077     return null;
2080 /**
2081  * Build the history chain for this course using the course originalcourseid.
2082  *
2083  * @param object $course course for which the history is returned.
2084  *
2085  * @return array ids of the source course in ancestry order, immediate parent 1st.
2086  */
2087 function get_course_history($course) {
2088     global $DB;
2089     $history = [];
2090     $parentid = $course->originalcourseid;
2091     while (!empty($parentid) && !in_array($parentid, $history)) {
2092         $history[] = $parentid;
2093         $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid));
2094     }
2095     return $history;
2098 /**
2099  * Used for building the names of the different custom parameters
2100  *
2101  * @param string $key   Parameter name
2102  * @param bool $tolower Do we want to convert the key into lower case?
2103  * @return string       Processed name
2104  */
2105 function lti_map_keyname($key, $tolower = true) {
2106     if ($tolower) {
2107         $newkey = '';
2108         $key = core_text::strtolower(trim($key));
2109         foreach (str_split($key) as $ch) {
2110             if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {
2111                 $newkey .= $ch;
2112             } else {
2113                 $newkey .= '_';
2114             }
2115         }
2116     } else {
2117         $newkey = $key;
2118     }
2119     return $newkey;
2122 /**
2123  * Gets the IMS role string for the specified user and LTI course module.
2124  *
2125  * @param mixed    $user      User object or user id
2126  * @param int      $cmid      The course module id of the LTI activity
2127  * @param int      $courseid  The course id of the LTI activity
2128  * @param boolean  $islti2    True if an LTI 2 tool is being launched
2129  *
2130  * @return string A role string suitable for passing with an LTI launch
2131  */
2132 function lti_get_ims_role($user, $cmid, $courseid, $islti2) {
2133     $roles = array();
2135     if (empty($cmid)) {
2136         // If no cmid is passed, check if the user is a teacher in the course
2137         // This allows other modules to programmatically "fake" a launch without
2138         // a real LTI instance.
2139         $context = context_course::instance($courseid);
2141         if (has_capability('moodle/course:manageactivities', $context, $user)) {
2142             array_push($roles, 'Instructor');
2143         } else {
2144             array_push($roles, 'Learner');
2145         }
2146     } else {
2147         $context = context_module::instance($cmid);
2149         if (has_capability('mod/lti:manage', $context)) {
2150             array_push($roles, 'Instructor');
2151         } else {
2152             array_push($roles, 'Learner');
2153         }
2154     }
2156     if (is_siteadmin($user) || has_capability('mod/lti:admin', $context)) {
2157         // Make sure admins do not have the Learner role, then set admin role.
2158         $roles = array_diff($roles, array('Learner'));
2159         if (!$islti2) {
2160             array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
2161         } else {
2162             array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator');
2163         }
2164     }
2166     return join(',', $roles);
2169 /**
2170  * Returns configuration details for the tool
2171  *
2172  * @param int $typeid   Basic LTI tool typeid
2173  *
2174  * @return array        Tool Configuration
2175  */
2176 function lti_get_type_config($typeid) {
2177     global $DB;
2179     $query = "SELECT name, value
2180                 FROM {lti_types_config}
2181                WHERE typeid = :typeid1
2182            UNION ALL
2183               SELECT 'toolurl' AS name, baseurl AS value
2184                 FROM {lti_types}
2185                WHERE id = :typeid2
2186            UNION ALL
2187               SELECT 'icon' AS name, icon AS value
2188                 FROM {lti_types}
2189                WHERE id = :typeid3
2190            UNION ALL
2191               SELECT 'secureicon' AS name, secureicon AS value
2192                 FROM {lti_types}
2193                WHERE id = :typeid4";
2195     $typeconfig = array();
2196     $configs = $DB->get_records_sql($query,
2197         array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid));
2199     if (!empty($configs)) {
2200         foreach ($configs as $config) {
2201             $typeconfig[$config->name] = $config->value;
2202         }
2203     }
2205     return $typeconfig;
2208 function lti_get_tools_by_url($url, $state, $courseid = null) {
2209     $domain = lti_get_domain_from_url($url);
2211     return lti_get_tools_by_domain($domain, $state, $courseid);
2214 function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
2215     global $DB, $SITE;
2217     $statefilter = '';
2218     $coursefilter = '';
2220     if ($state) {
2221         $statefilter = 'AND state = :state';
2222     }
2224     if ($courseid && $courseid != $SITE->id) {
2225         $coursefilter = 'OR course = :courseid';
2226     }
2228     $query = "SELECT *
2229                 FROM {lti_types}
2230                WHERE tooldomain = :tooldomain
2231                  AND (course = :siteid $coursefilter)
2232                  $statefilter";
2234     return $DB->get_records_sql($query, array(
2235         'courseid' => $courseid,
2236         'siteid' => $SITE->id,
2237         'tooldomain' => $domain,
2238         'state' => $state
2239     ));
2242 /**
2243  * Returns all basicLTI tools configured by the administrator
2244  *
2245  * @param int $course
2246  *
2247  * @return array
2248  */
2249 function lti_filter_get_types($course) {
2250     global $DB;
2252     if (!empty($course)) {
2253         $where = "WHERE t.course = :course";
2254         $params = array('course' => $course);
2255     } else {
2256         $where = '';
2257         $params = array();
2258     }
2259     $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname
2260                 FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id
2261                 {$where}";
2262     return $DB->get_records_sql($query, $params);
2265 /**
2266  * Given an array of tools, filter them based on their state
2267  *
2268  * @param array $tools An array of lti_types records
2269  * @param int $state One of the LTI_TOOL_STATE_* constants
2270  * @return array
2271  */
2272 function lti_filter_tool_types(array $tools, $state) {
2273     $return = array();
2274     foreach ($tools as $key => $tool) {
2275         if ($tool->state == $state) {
2276             $return[$key] = $tool;
2277         }
2278     }
2279     return $return;
2282 /**
2283  * Returns all lti types visible in this course
2284  *
2285  * @param int $courseid The id of the course to retieve types for
2286  * @param array $coursevisible options for 'coursevisible' field,
2287  *        default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]
2288  * @return stdClass[] All the lti types visible in the given course
2289  */
2290 function lti_get_lti_types_by_course($courseid, $coursevisible = null) {
2291     global $DB, $SITE;
2293     if ($coursevisible === null) {
2294         $coursevisible = [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER];
2295     }
2297     list($coursevisiblesql, $coursevisparams) = $DB->get_in_or_equal($coursevisible, SQL_PARAMS_NAMED, 'coursevisible');
2298     $courseconds = [];
2299     if (has_capability('mod/lti:addmanualinstance', context_course::instance($courseid))) {
2300         $courseconds[] = "course = :courseid";
2301     }
2302     if (has_capability('mod/lti:addpreconfiguredinstance', context_course::instance($courseid))) {
2303         $courseconds[] = "course = :siteid";
2304     }
2305     if (!$courseconds) {
2306         return [];
2307     }
2308     $coursecond = implode(" OR ", $courseconds);
2309     $query = "SELECT *
2310                 FROM {lti_types}
2311                WHERE coursevisible $coursevisiblesql
2312                  AND ($coursecond)
2313                  AND state = :active";
2315     return $DB->get_records_sql($query,
2316         array('siteid' => $SITE->id, 'courseid' => $courseid, 'active' => LTI_TOOL_STATE_CONFIGURED) + $coursevisparams);
2319 /**
2320  * Returns tool types for lti add instance and edit page
2321  *
2322  * @return array Array of lti types
2323  */
2324 function lti_get_types_for_add_instance() {
2325     global $COURSE;
2326     $admintypes = lti_get_lti_types_by_course($COURSE->id);
2328     $types = array();
2329     if (has_capability('mod/lti:addmanualinstance', context_course::instance($COURSE->id))) {
2330         $types[0] = (object)array('name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null);
2331     }
2333     foreach ($admintypes as $type) {
2334         $types[$type->id] = $type;
2335     }
2337     return $types;
2340 /**
2341  * Returns a list of configured types in the given course
2342  *
2343  * @param int $courseid The id of the course to retieve types for
2344  * @param int $sectionreturn section to return to for forming the URLs
2345  * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link
2346  */
2347 function lti_get_configured_types($courseid, $sectionreturn = 0) {
2348     global $OUTPUT;
2349     $types = array();
2350     $admintypes = lti_get_lti_types_by_course($courseid, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]);
2352     foreach ($admintypes as $ltitype) {
2353         $type           = new stdClass();
2354         $type->id       = $ltitype->id;
2355         $type->modclass = MOD_CLASS_ACTIVITY;
2356         $type->name     = 'lti_type_' . $ltitype->id;
2357         // Clean the name. We don't want tags here.
2358         $type->title    = clean_param($ltitype->name, PARAM_NOTAGS);
2359         $trimmeddescription = trim($ltitype->description);
2360         if ($trimmeddescription != '') {
2361             // Clean the description. We don't want tags here.
2362             $type->help     = clean_param($trimmeddescription, PARAM_NOTAGS);
2363             $type->helplink = get_string('modulename_shortcut_link', 'lti');
2364         }
2365         if (empty($ltitype->icon)) {
2366             $type->icon = $OUTPUT->pix_icon('icon', '', 'lti', array('class' => 'icon'));
2367         } else {
2368             $type->icon = html_writer::empty_tag('img', array('src' => $ltitype->icon, 'alt' => '', 'class' => 'icon'));
2369         }
2370         $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'course' => $courseid,
2371             'sr' => $sectionreturn, 'typeid' => $ltitype->id));
2372         $types[] = $type;
2373     }
2374     return $types;
2377 function lti_get_domain_from_url($url) {
2378     $matches = array();
2380     if (preg_match(LTI_URL_DOMAIN_REGEX, $url, $matches)) {
2381         return $matches[1];
2382     }
2385 function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) {
2386     $possibletools = lti_get_tools_by_url($url, $state, $courseid);
2388     return lti_get_best_tool_by_url($url, $possibletools, $courseid);
2391 function lti_get_url_thumbprint($url) {
2392     // Parse URL requires a schema otherwise everything goes into 'path'.  Fixed 5.4.7 or later.
2393     if (preg_match('/https?:\/\//', $url) !== 1) {
2394         $url = 'http://'.$url;
2395     }
2396     $urlparts = parse_url(strtolower($url));
2397     if (!isset($urlparts['path'])) {
2398         $urlparts['path'] = '';
2399     }
2401     if (!isset($urlparts['query'])) {
2402         $urlparts['query'] = '';
2403     }
2405     if (!isset($urlparts['host'])) {
2406         $urlparts['host'] = '';
2407     }
2409     if (substr($urlparts['host'], 0, 4) === 'www.') {
2410         $urlparts['host'] = substr($urlparts['host'], 4);
2411     }
2413     $urllower = $urlparts['host'] . '/' . $urlparts['path'];
2415     if ($urlparts['query'] != '') {
2416         $urllower .= '?' . $urlparts['query'];
2417     }
2419     return $urllower;
2422 function lti_get_best_tool_by_url($url, $tools, $courseid = null) {
2423     if (count($tools) === 0) {
2424         return null;
2425     }
2427     $urllower = lti_get_url_thumbprint($url);
2429     foreach ($tools as $tool) {
2430         $tool->_matchscore = 0;
2432         $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl);
2434         if ($urllower === $toolbaseurllower) {
2435             // 100 points for exact thumbprint match.
2436             $tool->_matchscore += 100;
2437         } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) {
2438             // 50 points if tool thumbprint starts with the base URL thumbprint.
2439             $tool->_matchscore += 50;
2440         }
2442         // Prefer course tools over site tools.
2443         if (!empty($courseid)) {
2444             // Minus 10 points for not matching the course id (global tools).
2445             if ($tool->course != $courseid) {
2446                 $tool->_matchscore -= 10;
2447             }
2448         }
2449     }
2451     $bestmatch = array_reduce($tools, function($value, $tool) {
2452         if ($tool->_matchscore > $value->_matchscore) {
2453             return $tool;
2454         } else {
2455             return $value;
2456         }
2458     }, (object)array('_matchscore' => -1));
2460     // None of the tools are suitable for this URL.
2461     if ($bestmatch->_matchscore <= 0) {
2462         return null;
2463     }
2465     return $bestmatch;
2468 function lti_get_shared_secrets_by_key($key) {
2469     global $DB;
2471     // Look up the shared secret for the specified key in both the types_config table (for configured tools)
2472     // And in the lti resource table for ad-hoc tools.
2473     $lti13 = LTI_VERSION_1P3;
2474     $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value
2475                 FROM {lti_types_config} t1
2476                 JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
2477                 JOIN {lti_types} type ON t2.typeid = type.id
2478               WHERE t1.name = 'resourcekey'
2479                 AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1
2480                 AND t2.name = 'password'
2481                 AND type.state = :configured1
2482                 AND type.ltiversion <> :ltiversion
2483                UNION
2484               SELECT tp.secret AS value
2485                 FROM {lti_tool_proxies} tp
2486                 JOIN {lti_types} t ON tp.id = t.toolproxyid
2487               WHERE tp.guid = :key2
2488                 AND t.state = :configured2
2489                UNION
2490               SELECT password AS value
2491                FROM {lti}
2492               WHERE resourcekey = :key3";
2494     $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13,
2495         'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));
2497     $values = array_map(function($item) {
2498         return $item->value;
2499     }, $sharedsecrets);
2501     // There should really only be one shared secret per key. But, we can't prevent
2502     // more than one getting entered. For instance, if the same key is used for two tool providers.
2503     return $values;
2506 /**
2507  * Delete a Basic LTI configuration
2508  *
2509  * @param int $id   Configuration id
2510  */
2511 function lti_delete_type($id) {
2512     global $DB;
2514     // We should probably just copy the launch URL to the tool instances in this case... using a single query.
2515     /*
2516     $instances = $DB->get_records('lti', array('typeid' => $id));
2517     foreach ($instances as $instance) {
2518         $instance->typeid = 0;
2519         $DB->update_record('lti', $instance);
2520     }*/
2522     $DB->delete_records('lti_types', array('id' => $id));
2523     $DB->delete_records('lti_types_config', array('typeid' => $id));
2526 function lti_set_state_for_type($id, $state) {
2527     global $DB;
2529     $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));
2532 /**
2533  * Transforms a basic LTI object to an array
2534  *
2535  * @param object $ltiobject    Basic LTI object
2536  *
2537  * @return array Basic LTI configuration details
2538  */
2539 function lti_get_config($ltiobject) {
2540     $typeconfig = (array)$ltiobject;
2541     $additionalconfig = lti_get_type_config($ltiobject->typeid);
2542     $typeconfig = array_merge($typeconfig, $additionalconfig);
2543     return $typeconfig;
2546 /**
2547  *
2548  * Generates some of the tool configuration based on the instance details
2549  *
2550  * @param int $id
2551  *
2552  * @return object configuration
2553  *
2554  */
2555 function lti_get_type_config_from_instance($id) {
2556     global $DB;
2558     $instance = $DB->get_record('lti', array('id' => $id));
2559     $config = lti_get_config($instance);
2561     $type = new \stdClass();
2562     $type->lti_fix = $id;
2563     if (isset($config['toolurl'])) {
2564         $type->lti_toolurl = $config['toolurl'];
2565     }
2566     if (isset($config['instructorchoicesendname'])) {
2567         $type->lti_sendname = $config['instructorchoicesendname'];
2568     }
2569     if (isset($config['instructorchoicesendemailaddr'])) {
2570         $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr'];
2571     }
2572     if (isset($config['instructorchoiceacceptgrades'])) {
2573         $type->lti_acceptgrades = $config['instructorchoiceacceptgrades'];
2574     }
2575     if (isset($config['instructorchoiceallowroster'])) {
2576         $type->lti_allowroster = $config['instructorchoiceallowroster'];
2577     }
2579     if (isset($config['instructorcustomparameters'])) {
2580         $type->lti_allowsetting = $config['instructorcustomparameters'];
2581     }
2582     return $type;
2585 /**
2586  * Generates some of the tool configuration based on the admin configuration details
2587  *
2588  * @param int $id
2589  *
2590  * @return stdClass Configuration details
2591  */
2592 function lti_get_type_type_config($id) {
2593     global $DB;
2595     $basicltitype = $DB->get_record('lti_types', array('id' => $id));
2596     $config = lti_get_type_config($id);
2598     $type = new \stdClass();
2600     $type->lti_typename = $basicltitype->name;
2602     $type->typeid = $basicltitype->id;
2604     $type->toolproxyid = $basicltitype->toolproxyid;
2606     $type->lti_toolurl = $basicltitype->baseurl;
2608     $type->lti_ltiversion = $basicltitype->ltiversion;
2610     $type->lti_clientid = $basicltitype->clientid;
2611     $type->lti_clientid_disabled = $type->lti_clientid;
2613     $type->lti_description = $basicltitype->description;
2615     $type->lti_parameters = $basicltitype->parameter;
2617     $type->lti_icon = $basicltitype->icon;
2619     $type->lti_secureicon = $basicltitype->secureicon;
2621     if (isset($config['resourcekey'])) {
2622         $type->lti_resourcekey = $config['resourcekey'];
2623     }
2624     if (isset($config['password'])) {
2625         $type->lti_password = $config['password'];
2626     }
2627     if (isset($config['publickey'])) {
2628         $type->lti_publickey = $config['publickey'];
2629     }
2630     if (isset($config['publickeyset'])) {
2631         $type->lti_publickeyset = $config['publickeyset'];
2632     }
2633     if (isset($config['keytype'])) {
2634         $type->lti_keytype = $config['keytype'];
2635     }
2636     if (isset($config['initiatelogin'])) {
2637         $type->lti_initiatelogin = $config['initiatelogin'];
2638     }
2639     if (isset($config['redirectionuris'])) {
2640         $type->lti_redirectionuris = $config['redirectionuris'];
2641     }
2643     if (isset($config['sendname'])) {
2644         $type->lti_sendname = $config['sendname'];
2645     }
2646     if (isset($config['instructorchoicesendname'])) {
2647         $type->lti_instructorchoicesendname = $config['instructorchoicesendname'];
2648     }
2649     if (isset($config['sendemailaddr'])) {
2650         $type->lti_sendemailaddr = $config['sendemailaddr'];
2651     }
2652     if (isset($config['instructorchoicesendemailaddr'])) {
2653         $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr'];
2654     }
2655     if (isset($config['acceptgrades'])) {
2656         $type->lti_acceptgrades = $config['acceptgrades'];
2657     }
2658     if (isset($config['instructorchoiceacceptgrades'])) {
2659         $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades'];
2660     }
2661     if (isset($config['allowroster'])) {
2662         $type->lti_allowroster = $config['allowroster'];
2663     }
2664     if (isset($config['instructorchoiceallowroster'])) {
2665         $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster'];
2666     }
2668     if (isset($config['customparameters'])) {
2669         $type->lti_customparameters = $config['customparameters'];
2670     }
2672     if (isset($config['forcessl'])) {
2673         $type->lti_forcessl = $config['forcessl'];
2674     }
2676     if (isset($config['organizationid_default'])) {
2677         $type->lti_organizationid_default = $config['organizationid_default'];
2678     } else {
2679         // Tool was configured before this option was available and the default then was host.
2680         $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;
2681     }
2682     if (isset($config['organizationid'])) {
2683         $type->lti_organizationid = $config['organizationid'];
2684     }
2685     if (isset($config['organizationurl'])) {
2686         $type->lti_organizationurl = $config['organizationurl'];
2687     }
2688     if (isset($config['organizationdescr'])) {
2689         $type->lti_organizationdescr = $config['organizationdescr'];
2690     }
2691     if (isset($config['launchcontainer'])) {
2692         $type->lti_launchcontainer = $config['launchcontainer'];
2693     }
2695     if (isset($config['coursevisible'])) {
2696         $type->lti_coursevisible = $config['coursevisible'];
2697     }
2699     if (isset($config['contentitem'])) {
2700         $type->lti_contentitem = $config['contentitem'];
2701     }
2703     if (isset($config['toolurl_ContentItemSelectionRequest'])) {
2704         $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];
2705     }
2707     if (isset($config['debuglaunch'])) {
2708         $type->lti_debuglaunch = $config['debuglaunch'];
2709     }
2711     if (isset($config['module_class_type'])) {
2712         $type->lti_module_class_type = $config['module_class_type'];
2713     }
2715     // Get the parameters from the LTI services.
2716     foreach ($config as $name => $value) {
2717         if (strpos($name, 'ltiservice_') === 0) {
2718             $type->{$name} = $config[$name];
2719         }
2720     }
2722     return $type;
2725 function lti_prepare_type_for_save($type, $config) {
2726     if (isset($config->lti_toolurl)) {
2727         $type->baseurl = $config->lti_toolurl;
2728         $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
2729     }
2730     if (isset($config->lti_description)) {
2731         $type->description = $config->lti_description;
2732     }
2733     if (isset($config->lti_typename)) {
2734         $type->name = $config->lti_typename;
2735     }
2736     if (isset($config->lti_ltiversion)) {
2737         $type->ltiversion = $config->lti_ltiversion;
2738     }
2739     if (isset($config->lti_clientid)) {
2740         $type->clientid = $config->lti_clientid;
2741     }
2742     if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {
2743         $type->clientid = random_string(15);
2744     } else if (empty($type->clientid)) {
2745         $type->clientid = null;
2746     }
2747     if (isset($config->lti_coursevisible)) {
2748         $type->coursevisible = $config->lti_coursevisible;
2749     }
2751     if (isset($config->lti_icon)) {
2752         $type->icon = $config->lti_icon;
2753     }
2754     if (isset($config->lti_secureicon)) {
2755         $type->secureicon = $config->lti_secureicon;
2756     }
2758     $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0;
2759     $config->lti_forcessl = $type->forcessl;
2760     if (isset($config->lti_contentitem)) {
2761         $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;
2762         $config->lti_contentitem = $type->contentitem;
2763     }
2764     if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
2765         if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {
2766             $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;
2767         } else {
2768             $type->toolurl_ContentItemSelectionRequest = '';
2769         }
2770         $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;
2771     }
2773     $type->timemodified = time();
2775     unset ($config->lti_typename);
2776     unset ($config->lti_toolurl);
2777     unset ($config->lti_description);
2778     unset ($config->lti_ltiversion);
2779     unset ($config->lti_clientid);
2780     unset ($config->lti_icon);
2781     unset ($config->lti_secureicon);
2784 function lti_update_type($type, $config) {
2785     global $DB, $CFG;
2787     lti_prepare_type_for_save($type, $config);
2789     if (lti_request_is_using_ssl() && !empty($type->secureicon)) {
2790         $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);
2791     } else {
2792         $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon));
2793     }
2794     unset($config->oldicon);
2796     if ($DB->update_record('lti_types', $type)) {
2797         foreach ($config as $key => $value) {
2798             if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {
2799                 $record = new \StdClass();
2800                 $record->typeid = $type->id;
2801                 $record->name = substr($key, 4);
2802                 $record->value = $value;
2803                 lti_update_config($record);
2804             }
2805             if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
2806                 $record = new \StdClass();
2807                 $record->typeid = $type->id;
2808                 $record->name = $key;
2809                 $record->value = $value;
2810                 lti_update_config($record);
2811             }
2812         }
2813         require_once($CFG->libdir.'/modinfolib.php');
2814         if ($clearcache) {
2815             $sql = "SELECT DISTINCT course
2816                       FROM {lti}
2817                      WHERE typeid = ?";
2819             $courses = $DB->get_fieldset_sql($sql, array($type->id));
2821             foreach ($courses as $courseid) {
2822                 rebuild_course_cache($courseid, true);
2823             }
2824         }
2825     }
2828 function lti_add_type($type, $config) {
2829     global $USER, $SITE, $DB;
2831     lti_prepare_type_for_save($type, $config);
2833     if (!isset($type->state)) {
2834         $type->state = LTI_TOOL_STATE_PENDING;
2835     }
2837     if (!isset($type->ltiversion)) {
2838         $type->ltiversion = LTI_VERSION_1;
2839     }
2841     if (!isset($type->timecreated)) {
2842         $type->timecreated = time();
2843     }
2845     if (!isset($type->createdby)) {
2846         $type->createdby = $USER->id;
2847     }
2849     if (!isset($type->course)) {
2850         $type->course = $SITE->id;
2851     }
2853     // Create a salt value to be used for signing passed data to extension services
2854     // The outcome service uses the service salt on the instance. This can be used
2855     // for communication with services not related to a specific LTI instance.
2856     $config->lti_servicesalt = uniqid('', true);
2858     $id = $DB->insert_record('lti_types', $type);
2860     if ($id) {
2861         foreach ($config as $key => $value) {
2862             if (!is_null($value)) {
2863                 if (substr($key, 0, 4) === 'lti_') {
2864                     $fieldname = substr($key, 4);
2865                 } else if (substr($key, 0, 11) !== 'ltiservice_') {
2866                     continue;
2867                 } else {
2868                     $fieldname = $key;
2869                 }
2871                 $record = new \StdClass();
2872                 $record->typeid = $id;
2873                 $record->name = $fieldname;
2874                 $record->value = $value;
2876                 lti_add_config($record);
2877             }
2878         }
2879     }
2881     return $id;
2884 /**
2885  * Given an array of tool proxies, filter them based on their state
2886  *
2887  * @param array $toolproxies An array of lti_tool_proxies records
2888  * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants
2889  *
2890  * @return array
2891  */
2892 function lti_filter_tool_proxy_types(array $toolproxies, $state) {
2893     $return = array();
2894     foreach ($toolproxies as $key => $toolproxy) {
2895         if ($toolproxy->state == $state) {
2896             $return[$key] = $toolproxy;
2897         }
2898     }
2899     return $return;
2902 /**
2903  * Get the tool proxy instance given its GUID
2904  *
2905  * @param string  $toolproxyguid   Tool proxy GUID value
2906  *
2907  * @return object
2908  */
2909 function lti_get_tool_proxy_from_guid($toolproxyguid) {
2910     global $DB;
2912     $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid));
2914     return $toolproxy;
2917 /**
2918  * Get the tool proxy instance given its registration URL
2919  *
2920  * @param string $regurl Tool proxy registration URL
2921  *
2922  * @return array The record of the tool proxy with this url
2923  */
2924 function lti_get_tool_proxies_from_registration_url($regurl) {
2925     global $DB;
2927     return $DB->get_records_sql(
2928         'SELECT * FROM {lti_tool_proxies}
2929         WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl',
2930         array('regurl' => $regurl)
2931     );
2934 /**
2935  * Generates some of the tool proxy configuration based on the admin configuration details
2936  *
2937  * @param int $id
2938  *
2939  * @return mixed Tool Proxy details
2940  */
2941 function lti_get_tool_proxy($id) {
2942     global $DB;
2944     $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id));
2945     return $toolproxy;
2948 /**
2949  * Returns lti tool proxies.
2950  *
2951  * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them
2952  * @return array of basicLTI types
2953  */
2954 function lti_get_tool_proxies($orphanedonly) {
2955     global $DB;
2957     if ($orphanedonly) {
2958         $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));
2959         $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
2960         foreach ($proxies as $key => $value) {
2961             if (in_array($value->id, $usedproxyids)) {
2962                 unset($proxies[$key]);
2963             }
2964         }
2965         return $proxies;
2966     } else {
2967         return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
2968     }
2971 /**
2972  * Generates some of the tool proxy configuration based on the admin configuration details
2973  *
2974  * @param int $id
2975  *
2976  * @return mixed  Tool Proxy details
2977  */
2978 function lti_get_tool_proxy_config($id) {
2979     $toolproxy = lti_get_tool_proxy($id);
2981     $tp = new \stdClass();
2982     $tp->lti_registrationname = $toolproxy->name;
2983     $tp->toolproxyid = $toolproxy->id;
2984     $tp->state = $toolproxy->state;
2985     $tp->lti_registrationurl = $toolproxy->regurl;
2986     $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered);
2987     $tp->lti_services = explode("\n", $toolproxy->serviceoffered);
2989     return $tp;
2992 /**
2993  * Update the database with a tool proxy instance
2994  *
2995  * @param object   $config    Tool proxy definition
2996  *
2997  * @return int  Record id number
2998  */
2999 function lti_add_tool_proxy($config) {
3000     global $USER, $DB;
3002     $toolproxy = new \stdClass();
3003     if (isset($config->lti_registrationname)) {
3004         $toolproxy->name = trim($config->lti_registrationname);
3005     }
3006     if (isset($config->lti_registrationurl)) {
3007         $toolproxy->regurl = trim($config->lti_registrationurl);
3008     }
3009     if (isset($config->lti_capabilities)) {
3010         $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities);
3011     } else {
3012         $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities()));
3013     }
3014     if (isset($config->lti_services)) {
3015         $toolproxy->serviceoffered = implode("\n", $config->lti_services);
3016     } else {
3017         $func = function($s) {
3018             return $s->get_id();
3019         };
3020         $servicenames = array_map($func, lti_get_services());
3021         $toolproxy->serviceoffered = implode("\n", $servicenames);
3022     }
3023     if (isset($config->toolproxyid) && !empty($config->toolproxyid)) {
3024         $toolproxy->id = $config->toolproxyid;
3025         if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) {
3026             $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3027             $toolproxy->guid = random_string();
3028             $toolproxy->secret = random_string();
3029         }
3030         $id = lti_update_tool_proxy($toolproxy);
3031     } else {
3032         $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
3033         $toolproxy->timemodified = time();
3034         $toolproxy->timecreated = $toolproxy->timemodified;
3035         if (!isset($toolproxy->createdby)) {
3036             $toolproxy->createdby = $USER->id;
3037         }
3038         $toolproxy->guid = random_string();
3039         $toolproxy->secret = random_string();
3040         $id = $DB->insert_record('lti_tool_proxies', $toolproxy);
3041     }
3043     return $id;
3046 /**
3047  * Updates a tool proxy in the database
3048  *
3049  * @param object  $toolproxy   Tool proxy
3050  *
3051  * @return int    Record id number
3052  */
3053 function lti_update_tool_proxy($toolproxy) {
3054     global $DB;
3056     $toolproxy->timemodified = time();
3057     $id = $DB->update_record('lti_tool_proxies', $toolproxy);
3059     return $id;
3062 /**
3063  * Delete a Tool Proxy
3064  *
3065  * @param int $id   Tool Proxy id
3066  */
3067 function lti_delete_tool_proxy($id) {
3068     global $DB;
3069     $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id));
3070     $tools = $DB->get_records('lti_types', array('toolproxyid' => $id));
3071     foreach ($tools as $tool) {
3072         lti_delete_type($tool->id);
3073     }
3074     $DB->delete_records('lti_tool_proxies', array('id' => $id));
3077 /**
3078  * Add a tool configuration in the database
3079  *
3080  * @param object $config   Tool configuration
3081  *
3082  * @return int Record id number
3083  */
3084 function lti_add_config($config) {
3085     global $DB;
3087     return $DB->insert_record('lti_types_config', $config);
3090 /**
3091  * Updates a tool configuration in the database
3092  *
3093  * @param object  $config   Tool configuration
3094  *
3095  * @return mixed Record id number
3096  */
3097 function lti_update_config($config) {
3098     global $DB;
3100     $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));
3102     if ($old) {
3103         $config->id = $old->id;
3104         $return = $DB->update_record('lti_types_config', $config);
3105     } else {
3106         $return = $DB->insert_record('lti_types_config', $config);
3107     }
3108     return $return;
3111 /**
3112  * Gets the tool settings
3113  *
3114  * @param int  $toolproxyid   Id of tool proxy record (or tool ID if negative)
3115  * @param int  $courseid      Id of course (null if system settings)
3116  * @param int  $instanceid    Id of course module (null if system or context settings)
3117  *
3118  * @return array  Array settings
3119  */
3120 function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) {
3121     global $DB;
3123     $settings = array();
3124     if ($toolproxyid > 0) {
3125         $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
3126             'course' => $courseid, 'coursemoduleid' => $instanceid));
3127     } else {
3128         $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid,
3129             'course' => $courseid, 'coursemoduleid' => $instanceid));
3130     }
3131     if ($settingsstr !== false) {
3132         $settings = json_decode($settingsstr, true);
3133     }
3134     return $settings;
3137 /**
3138  * Sets the tool settings (
3139  *
3140  * @param array  $settings      Array of settings
3141  * @param int    $toolproxyid   Id of tool proxy record (or tool ID if negative)
3142  * @param int    $courseid      Id of course (null if system settings)
3143  * @param int    $instanceid    Id of course module (null if system or context settings)
3144  */
3145 function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) {
3146     global $DB;
3148     $json = json_encode($settings);
3149     if ($toolproxyid >= 0) {
3150         $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
3151             'course' => $courseid, 'coursemoduleid' => $instanceid));
3152     } else {
3153         $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid,
3154             'course' => $courseid, 'coursemoduleid' => $instanceid));
3155     }
3156     if ($record !== false) {
3157         $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
3158     } else {
3159         $record = new \stdClass();
3160         if ($toolproxyid > 0) {
3161             $record->toolproxyid = $toolproxyid;
3162         } else {
3163             $record->typeid = -$toolproxyid;
3164         }
3165         $record->course = $courseid;
3166         $record->coursemoduleid = $instanceid;
3167         $record->settings = $json;
3168         $record->timecreated = time();
3169         $record->timemodified = $record->timecreated;
3170         $DB->insert_record('lti_tool_settings', $record);
3171     }
3174 /**
3175  * Signs the petition to launch the external tool using OAuth
3176  *
3177  * @param array  $oldparms     Parameters to be passed for signing
3178  * @param string $endpoint     url of the external tool
3179  * @param string $method       Method for sending the parameters (e.g. POST)
3180  * @param string $oauthconsumerkey
3181  * @param string $oauthconsumersecret
3182  * @return array|null
3183  */
3184 function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
3186     $parms = $oldparms;
3188     $testtoken = '';
3190     // TODO: Switch to core oauthlib once implemented - MDL-30149.
3191     $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1();
3192     $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null);
3193     $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms);
3194     $accreq->sign_request($hmacmethod, $testconsumer, $testtoken);
3196     $newparms = $accreq->get_parameters();
3198     return $newparms;
3201 /**
3202  * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT
3203  *
3204  * @param array  $parms        Parameters to be passed for signing
3205  * @param string $endpoint     url of the external tool
3206  * @param string $oauthconsumerkey
3207  * @param string $typeid       ID of LTI tool type
3208  * @param string $nonce        Nonce value to use
3209  * @return array|null
3210  */
3211 function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {
3212     global $CFG;
3214     if (empty($typeid)) {
3215         $typeid = 0;
3216     }
3217     $messagetypemapping = lti_get_jwt_message_type_mapping();
3218     if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) {
3219         $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']];
3220     }
3221     if (isset($parms['roles'])) {
3222         $roles = explode(',', $parms['roles']);
3223         $newroles = array();
3224         foreach ($roles as $role) {
3225             if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {
3226                 $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);
3227             } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {
3228                 $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);
3229             } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {
3230                 $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);
3231             } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {
3232                 $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";
3233             }
3234             $newroles[] = $role;
3235         }
3236         $parms['roles'] = implode(',', $newroles);
3237     }
3239     $now = time();
3240     if (empty($nonce)) {
3241         $nonce = bin2hex(openssl_random_pseudo_bytes(10));
3242     }
3243     $claimmapping = lti_get_jwt_claim_mapping();
3244     $payload = array(
3245         'nonce' => $nonce,
3246         'iat' => $now,
3247         'exp' => $now + 60,
3248     );
3249     $payload['iss'] = $CFG->wwwroot;
3250     $payload['aud'] = $oauthconsumerkey;
3251     $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid);
3252     $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;
3254     foreach ($parms as $key => $value) {
3255         $claim = LTI_JWT_CLAIM_PREFIX;
3256         if (array_key_exists($key, $claimmapping)) {
3257             $mapping = $claimmapping[$key];
3258             $type = $mapping["type"] ?? "string";
3259             if ($mapping['isarray']) {
3260                 $value = explode(',', $value);
3261                 sort($value);
3262             } else if ($type == 'boolean') {
3263                 $value = isset($value) && ($value == 'true');
3264             }
3265             if (!empty($mapping['suffix'])) {
3266                 $claim .= "-{$mapping['suffix']}";
3267             }
3268             $claim .= '/claim/';
3269             if (is_null($mapping['group'])) {
3270                 $payload[$mapping['claim']] = $value;
3271             } else if (empty($mapping['group'])) {
3272                 $payload["{$claim}{$mapping['claim']}"] = $value;
3273             } else {
3274                 $claim .= $mapping['group'];
3275                 $payload[$claim][$mapping['claim']] = $value;
3276             }
3277         } else if (strpos($key, 'custom_') === 0) {
3278             $payload["{$claim}/claim/custom"][substr($key, 7)] = $value;
3279         } else if (strpos($key, 'ext_') === 0) {
3280             $payload["{$claim}/claim/ext"][substr($key, 4)] = $value;
3281         }
3282     }
3284     $privatekey = get_config('mod_lti', 'privatekey');
3285     $kid = get_config('mod_lti', 'kid');
3286     $jwt = JWT::encode($payload, $privatekey, 'RS256', $kid);
3288     $newparms = array();
3289     $newparms['id_token'] = $jwt;
3291     return $newparms;
3294 /**
3295  * Verfies the JWT and converts its claims to their equivalent message parameter.
3296  *
3297  * @param int    $typeid
3298  * @param string $jwtparam   JWT parameter
3299  *
3300  * @return array  message parameters
3301  * @throws moodle_exception
3302  */
3303 function lti_convert_from_jwt($typeid, $jwtparam) {
3305     $params = array();
3306     $parts = explode('.', $jwtparam);
3307     $ok = (count($parts) === 3);
3308     if ($ok) {
3309         $payload = JWT::urlsafeB64Decode($parts[1]);
3310         $claims = json_decode($payload, true);
3311         $ok = !is_null($claims) && !empty($claims['iss']);
3312     }
3313     if ($ok) {
3314         lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);
3315         $params['oauth_consumer_key'] = $claims['iss'];
3316         foreach (lti_get_jwt_claim_mapping() as $key => $mapping) {
3317             $claim = LTI_JWT_CLAIM_PREFIX;
3318             if (!empty($mapping['suffix'])) {
3319                 $claim .= "-{$mapping['suffix']}";
3320             }
3321             $claim .= '/claim/';
3322             if (is_null($mapping['group'])) {
3323                 $claim = $mapping['claim'];
3324             } else if (empty($mapping['group'])) {
3325                 $claim .= $mapping['claim'];
3326             } else {
3327                 $claim .= $mapping['group'];
3328             }
3329             if (isset($claims[$claim])) {
3330                 $value = null;
3331                 if (empty($mapping['group'])) {
3332                     $value = $claims[$claim];
3333                 } else {
3334                     $group = $claims[$claim];
3335                     if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
3336                         $value = $group[$mapping['claim']];
3337                     }
3338                 }
3339                 if (!empty($value) && $mapping['isarray']) {
3340                     if (is_array($value)) {
3341                         if (is_array($value[0])) {
3342                             $value = json_encode($value);
3343                         } else {
3344                             $value = implode(',', $value);
3345                         }
3346                     }
3347                 }
3348                 if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {
3349                     $params[$key] = $value;
3350                 }
3351             }
3352             $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';
3353             if (isset($claims[$claim])) {
3354                 $custom = $claims[$claim];
3355                 if (is_array($custom)) {
3356                     foreach ($custom as $key => $value) {
3357                         $params["custom_{$key}"] = $value;
3358                     }
3359                 }
3360             }
3361             $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';
3362             if (isset($claims[$claim])) {
3363                 $ext = $claims[$claim];
3364                 if (is_array($ext)) {
3365                     foreach ($ext as $key => $value) {
3366                         $params["ext_{$key}"] = $value;
3367                     }
3368                 }
3369             }
3370         }
3371     }
3372     if (isset($params['content_items'])) {
3373         $params['content_items'] = lti_convert_content_items($params['content_items']);
3374     }
3375     $messagetypemapping = lti_get_jwt_message_type_mapping();
3376     if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) {
3377         $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']];
3378     }
3379     return $params;
3382 /**
3383  * Posts the launch petition HTML
3384  *
3385  * @param array $newparms   Signed parameters
3386  * @param string $endpoint  URL of the external tool
3387  * @param bool $debug       Debug (true/false)
3388  * @return string
3389  */
3390 function lti_post_launch_html($newparms, $endpoint, $debug=false) {
3391     $r = "<form action=\"" . $endpoint .
3392         "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n";
3394     // Contruct html for the launch parameters.
3395     foreach ($newparms as $key => $value) {
3396         $key = htmlspecialchars($key);
3397         $value = htmlspecialchars($value);
3398         if ( $key == "ext_submit" ) {
3399             $r .= "<input type=\"submit\"";
3400         } else {
3401             $r .= "<input type=\"hidden\" name=\"{$key}\"";
3402         }
3403         $r .= " value=\"";
3404         $r .= $value;
3405         $r .= "\"/>\n";
3406     }
3408     if ( $debug ) {
3409         $r .= "<script language=\"javascript\"> \n";
3410         $r .= "  //<![CDATA[ \n";
3411         $r .= "function basicltiDebugToggle() {\n";
3412         $r .= "    var ele = document.getElementById(\"basicltiDebug\");\n";
3413         $r .= "    if (ele.style.display == \"block\") {\n";
3414         $r .= "        ele.style.display = \"none\";\n";
3415         $r .= "    }\n";
3416         $r .= "    else {\n";
3417         $r .= "        ele.style.display = \"block\";\n";
3418         $r .= "    }\n";
3419         $r .= "} \n";
3420         $r .= "  //]]> \n";
3421         $r .= "</script>\n";
3422         $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";
3423         $r .= get_string("toggle_debug_data", "lti")."</a>\n";
3424         $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
3425         $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n";
3426         $r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
3427         $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n";
3428         foreach ($newparms as $key => $value) {
3429             $key = htmlspecialchars($key);
3430             $value = htmlspecialchars($value);
3431             $r .= "$key = $value<br/>\n";
3432         }
3433         $r .= "&nbsp;<br/>\n";
3434         $r .= "</div>\n";
3435     }
3436     $r .= "</form>\n";
3438     if ( ! $debug ) {
3439         $r .= " <script type=\"text/javascript\"> \n" .
3440             "  //<![CDATA[ \n" .
3441             "    document.ltiLaunchForm.submit(); \n" .
3442             "  //]]> \n" .
3443             " </script> \n";
3444     }
3445     return $r;
3448 /**
3449  * Generate the form for initiating a login request for an LTI 1.3 message
3450  *
3451  * @param int            $courseid  Course ID
3452  * @param int            $id        LTI instance ID
3453  * @param stdClass|null  $instance  LTI instance
3454  * @param stdClass       $config    Tool type configuration
3455  * @param string         $messagetype   LTI message type
3456  * @param string         $title     Title of content item
3457  * @param string         $text      Description of content item
3458  * @return string
3459  */
3460 function lti_initiate_login($courseid, $id, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '',
3461         $text = '') {
3462     global $SESSION;
3464     $params = lti_build_login_request($courseid, $id, $instance, $config, $messagetype);
3465     $SESSION->lti_message_hint = "{$courseid},{$config->typeid},{$id}," . base64_encode($title) . ',' .
3466         base64_encode($text);
3468     $r = "<form action=\"" . $config->lti_initiatelogin .
3469         "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " .
3470         "encType=\"application/x-www-form-urlencoded\">\n";
3472     foreach ($params as $key => $value) {
3473         $key = htmlspecialchars($key);
3474         $value = htmlspecialchars($value);
3475         $r .= "  <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
3476     }
3477     $r .= "</form>\n";
3479     $r .= "<script type=\"text/javascript\">\n" .
3480         "//<![CDATA[\n" .
3481         "document.ltiInitiateLoginForm.submit();\n" .
3482         "//]]>\n" .
3483         "</script>\n";
3485     return $r;
3488 /**
3489  * Prepares an LTI 1.3 login request
3490  *
3491  * @param int            $courseid  Course ID
3492  * @param int            $id        LTI instance ID
3493  * @param stdClass|null  $instance  LTI instance
3494  * @param stdClass       $config    Tool type configuration
3495  * @param string         $messagetype   LTI message type
3496  * @return array Login request parameters
3497  */
3498 function lti_build_login_request($courseid, $id, $instance, $config, $messagetype) {
3499     global $USER, $CFG;
3501     if (!empty($instance)) {
3502         $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;
3503     } else {
3504         $endpoint = $config->lti_toolurl;
3505         if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) {
3506             $endpoint = $config->lti_toolurl_ContentItemSelectionRequest;
3507         }
3508     }
3509     $endpoint = trim($endpoint);
3511     // If SSL is forced make sure https is on the normal launch URL.
3512     if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {
3513         $endpoint = lti_ensure_url_is_https($endpoint);
3514     } else if (!strstr($endpoint, '://')) {
3515         $endpoint = 'http://' . $endpoint;
3516     }
3518     $params = array();
3519     $params['iss'] = $CFG->wwwroot;
3520     $params['target_link_uri'] = $endpoint;
3521     $params['login_hint'] = $USER->id;
3522     $params['lti_message_hint'] = $id;
3523     $params['client_id'] = $config->lti_clientid;
3524     $params['lti_deployment_id'] = $config->typeid;
3525     return $params;
3528 function lti_get_type($typeid) {
3529     global $DB;
3531     return $DB->get_record('lti_types', array('id' => $typeid));
3534 function lti_get_launch_container($lti, $toolconfig) {
3535     if (empty($lti->launchcontainer)) {
3536         $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
3537     }
3539     if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
3540         if (isset($toolconfig['launchcontainer'])) {
3541             $launchcontainer = $toolconfig['launchcontainer'];
3542         }