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