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