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