Merge branch 'MDL-61708-lti-fullnamedisplay' of https://github.com/wjroes/moodle
[moodle.git] / mod / lti / locallib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 //
17 // This file is part of BasicLTI4Moodle
18 //
19 // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
20 // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
21 // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
22 // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
23 // are already supporting or going to support BasicLTI. This project Implements the consumer
24 // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
25 // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
26 // at the GESSI research group at UPC.
27 // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
28 // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
29 // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
30 //
31 // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
32 // of the Universitat Politecnica de Catalunya http://www.upc.edu
33 // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu.
35 /**
36  * This file contains the library of functions and constants for the lti module
37  *
38  * @package mod_lti
39  * @copyright  2009 Marc Alier, Jordi Piguillem, Nikolas Galanis
40  *  marc.alier@upc.edu
41  * @copyright  2009 Universitat Politecnica de Catalunya http://www.upc.edu
42  * @author     Marc Alier
43  * @author     Jordi Piguillem
44  * @author     Nikolas Galanis
45  * @author     Chris Scribner
46  * @copyright  2015 Vital Source Technologies http://vitalsource.com
47  * @author     Stephen Vickers
48  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49  */
51 defined('MOODLE_INTERNAL') || die;
53 // TODO: Switch to core oauthlib once implemented - MDL-30149.
54 use moodle\mod\lti as lti;
56 global $CFG;
57 require_once($CFG->dirroot.'/mod/lti/OAuth.php');
58 require_once($CFG->libdir.'/weblib.php');
59 require_once($CFG->dirroot . '/course/modlib.php');
60 require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
62 define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i');
64 define('LTI_LAUNCH_CONTAINER_DEFAULT', 1);
65 define('LTI_LAUNCH_CONTAINER_EMBED', 2);
66 define('LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS', 3);
67 define('LTI_LAUNCH_CONTAINER_WINDOW', 4);
68 define('LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW', 5);
70 define('LTI_TOOL_STATE_ANY', 0);
71 define('LTI_TOOL_STATE_CONFIGURED', 1);
72 define('LTI_TOOL_STATE_PENDING', 2);
73 define('LTI_TOOL_STATE_REJECTED', 3);
74 define('LTI_TOOL_PROXY_TAB', 4);
76 define('LTI_TOOL_PROXY_STATE_CONFIGURED', 1);
77 define('LTI_TOOL_PROXY_STATE_PENDING', 2);
78 define('LTI_TOOL_PROXY_STATE_ACCEPTED', 3);
79 define('LTI_TOOL_PROXY_STATE_REJECTED', 4);
81 define('LTI_SETTING_NEVER', 0);
82 define('LTI_SETTING_ALWAYS', 1);
83 define('LTI_SETTING_DELEGATE', 2);
85 define('LTI_COURSEVISIBLE_NO', 0);
86 define('LTI_COURSEVISIBLE_PRECONFIGURED', 1);
87 define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2);
89 define('LTI_VERSION_1', 'LTI-1p0');
90 define('LTI_VERSION_2', 'LTI-2p0');
92 /**
93  * Return the launch data required for opening the external tool.
94  *
95  * @param  stdClass $instance the external tool activity settings
96  * @return array the endpoint URL and parameters (including the signature)
97  * @since  Moodle 3.0
98  */
99 function lti_get_launch_data($instance) {
100     global $PAGE, $CFG, $USER;
102     if (empty($instance->typeid)) {
103         $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
104         if ($tool) {
105             $typeid = $tool->id;
106         } else {
107             $tool = lti_get_tool_by_url_match($instance->securetoolurl,  $instance->course);
108             if ($tool) {
109                 $typeid = $tool->id;
110             } else {
111                 $typeid = null;
112             }
113         }
114     } else {
115         $typeid = $instance->typeid;
116         $tool = lti_get_type($typeid);
117     }
119     if ($typeid) {
120         $typeconfig = lti_get_type_config($typeid);
121     } else {
122         // There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults.
123         $typeconfig = (array)$instance;
125         $typeconfig['sendname'] = $instance->instructorchoicesendname;
126         $typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr;
127         $typeconfig['customparameters'] = $instance->instructorcustomparameters;
128         $typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades;
129         $typeconfig['allowroster'] = $instance->instructorchoiceallowroster;
130         $typeconfig['forcessl'] = '0';
131     }
133     // Default the organizationid if not specified.
134     if (empty($typeconfig['organizationid'])) {
135         $urlparts = parse_url($CFG->wwwroot);
137         $typeconfig['organizationid'] = $urlparts['host'];
138     }
140     if (isset($tool->toolproxyid)) {
141         $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
142         $key = $toolproxy->guid;
143         $secret = $toolproxy->secret;
144     } else {
145         $toolproxy = null;
146         if (!empty($instance->resourcekey)) {
147             $key = $instance->resourcekey;
148         } else if (!empty($typeconfig['resourcekey'])) {
149             $key = $typeconfig['resourcekey'];
150         } else {
151             $key = '';
152         }
153         if (!empty($instance->password)) {
154             $secret = $instance->password;
155         } else if (!empty($typeconfig['password'])) {
156             $secret = $typeconfig['password'];
157         } else {
158             $secret = '';
159         }
160     }
162     $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl'];
163     $endpoint = trim($endpoint);
165     // If the current request is using SSL and a secure tool URL is specified, use it.
166     if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) {
167         $endpoint = trim($instance->securetoolurl);
168     }
170     // If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL.
171     if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
172         if (!empty($instance->securetoolurl)) {
173             $endpoint = trim($instance->securetoolurl);
174         }
176         $endpoint = lti_ensure_url_is_https($endpoint);
177     } else {
178         if (!strstr($endpoint, '://')) {
179             $endpoint = 'http://' . $endpoint;
180         }
181     }
183     $orgid = $typeconfig['organizationid'];
185     $course = $PAGE->course;
186     $islti2 = isset($tool->toolproxyid);
187     $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2);
188     if ($islti2) {
189         $requestparams = lti_build_request_lti2($tool, $allparams);
190     } else {
191         $requestparams = $allparams;
192     }
193     $requestparams = array_merge($requestparams, lti_build_standard_request($instance, $orgid, $islti2));
194     $customstr = '';
195     if (isset($typeconfig['customparameters'])) {
196         $customstr = $typeconfig['customparameters'];
197     }
198     $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr,
199         $instance->instructorcustomparameters, $islti2));
201     $launchcontainer = lti_get_launch_container($instance, $typeconfig);
202     $returnurlparams = array('course' => $course->id,
203                              'launch_container' => $launchcontainer,
204                              'instanceid' => $instance->id,
205                              'sesskey' => sesskey());
207     // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns.
208     $url = new \moodle_url('/mod/lti/return.php', $returnurlparams);
209     $returnurl = $url->out(false);
211     if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
212         $returnurl = lti_ensure_url_is_https($returnurl);
213     }
215     $target = '';
216     switch($launchcontainer) {
217         case LTI_LAUNCH_CONTAINER_EMBED:
218         case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS:
219             $target = 'iframe';
220             break;
221         case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW:
222             $target = 'frame';
223             break;
224         case LTI_LAUNCH_CONTAINER_WINDOW:
225             $target = 'window';
226             break;
227     }
228     if (!empty($target)) {
229         $requestparams['launch_presentation_document_target'] = $target;
230     }
232     $requestparams['launch_presentation_return_url'] = $returnurl;
234     // Add the parameters configured by the LTI advantage services.
235     if ($typeid && !$islti2) {
236         $services = lti_get_services();
237         foreach ($services as $service) {
238             $ltiadvantageparameters = $service->get_launch_parameters('basic-lti-launch-request',
239                     $course->id, $USER->id , $typeid, $instance->id);
240             foreach ($ltiadvantageparameters as $ltiadvantagekey => $ltiadvantagevalue) {
241                 $requestparams[$ltiadvantagekey] = $ltiadvantagevalue;
242             }
243         }
244     }
246     // Allow request params to be updated by sub-plugins.
247     $plugins = core_component::get_plugin_list('ltisource');
248     foreach (array_keys($plugins) as $plugin) {
249         $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch',
250             array($instance, $endpoint, $requestparams), array());
252         if (!empty($pluginparams) && is_array($pluginparams)) {
253             $requestparams = array_merge($requestparams, $pluginparams);
254         }
255     }
257     if (!empty($key) && !empty($secret)) {
258         $parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret);
260         $endpointurl = new \moodle_url($endpoint);
261         $endpointparams = $endpointurl->params();
263         // Strip querystring params in endpoint url from $parms to avoid duplication.
264         if (!empty($endpointparams) && !empty($parms)) {
265             foreach (array_keys($endpointparams) as $paramname) {
266                 if (isset($parms[$paramname])) {
267                     unset($parms[$paramname]);
268                 }
269             }
270         }
272     } else {
273         // If no key and secret, do the launch unsigned.
274         $returnurlparams['unsigned'] = '1';
275         $parms = $requestparams;
276     }
278     return array($endpoint, $parms);
281 /**
282  * Launch an external tool activity.
283  *
284  * @param  stdClass $instance the external tool activity settings
285  * @return string The HTML code containing the javascript code for the launch
286  */
287 function lti_launch_tool($instance) {
289     list($endpoint, $parms) = lti_get_launch_data($instance);
290     $debuglaunch = ( $instance->debuglaunch == 1 );
292     $content = lti_post_launch_html($parms, $endpoint, $debuglaunch);
294     echo $content;
297 /**
298  * Prepares an LTI registration request message
299  *
300  * @param object $toolproxy  Tool Proxy instance object
301  */
302 function lti_register($toolproxy) {
303     $endpoint = $toolproxy->regurl;
305     // Change the status to pending.
306     $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING;
307     lti_update_tool_proxy($toolproxy);
309     $requestparams = lti_build_registration_request($toolproxy);
311     $content = lti_post_launch_html($requestparams, $endpoint, false);
313     echo $content;
317 /**
318  * Gets the parameters for the regirstration request
319  *
320  * @param object $toolproxy Tool Proxy instance object
321  * @return array Registration request parameters
322  */
323 function lti_build_registration_request($toolproxy) {
324     $key = $toolproxy->guid;
325     $secret = $toolproxy->secret;
327     $requestparams = array();
328     $requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest';
329     $requestparams['lti_version'] = 'LTI-2p0';
330     $requestparams['reg_key'] = $key;
331     $requestparams['reg_password'] = $secret;
332     $requestparams['reg_url'] = $toolproxy->regurl;
334     // Add the profile URL.
335     $profileservice = lti_get_service_by_name('profile');
336     $profileservice->set_tool_proxy($toolproxy);
337     $requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url');
339     // Add the return URL.
340     $returnurlparams = array('id' => $toolproxy->id, 'sesskey' => sesskey());
341     $url = new \moodle_url('/mod/lti/externalregistrationreturn.php', $returnurlparams);
342     $returnurl = $url->out(false);
344     $requestparams['launch_presentation_return_url'] = $returnurl;
346     return $requestparams;
349 /**
350  * Build source ID
351  *
352  * @param int $instanceid
353  * @param int $userid
354  * @param string $servicesalt
355  * @param null|int $typeid
356  * @param null|int $launchid
357  * @return stdClass
358  */
359 function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) {
360     $data = new \stdClass();
362     $data->instanceid = $instanceid;
363     $data->userid = $userid;
364     $data->typeid = $typeid;
365     if (!empty($launchid)) {
366         $data->launchid = $launchid;
367     } else {
368         $data->launchid = mt_rand();
369     }
371     $json = json_encode($data);
373     $hash = hash('sha256', $json . $servicesalt, false);
375     $container = new \stdClass();
376     $container->data = $data;
377     $container->hash = $hash;
379     return $container;
382 /**
383  * This function builds the request that must be sent to the tool producer
384  *
385  * @param object    $instance       Basic LTI instance object
386  * @param array     $typeconfig     Basic LTI tool configuration
387  * @param object    $course         Course object
388  * @param int|null  $typeid         Basic LTI tool ID
389  * @param boolean   $islti2         True if an LTI 2 tool is being launched
390  *
391  * @return array                    Request details
392  */
393 function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false) {
394     global $USER, $CFG;
396     if (empty($instance->cmid)) {
397         $instance->cmid = 0;
398     }
400     $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2);
402     $requestparams = array(
403         'user_id' => $USER->id,
404         'lis_person_sourcedid' => $USER->idnumber,
405         'roles' => $role,
406         'context_id' => $course->id,
407         'context_label' => trim(html_to_text($course->shortname, 0)),
408         'context_title' => trim(html_to_text($course->fullname, 0)),
409     );
410     if (!empty($instance->name)) {
411         $requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0));
412     }
413     if (!empty($instance->cmid)) {
414         $intro = format_module_intro('lti', $instance, $instance->cmid);
415         $intro = trim(html_to_text($intro, 0, false));
417         // This may look weird, but this is required for new lines
418         // so we generate the same OAuth signature as the tool provider.
419         $intro = str_replace("\n", "\r\n", $intro);
420         $requestparams['resource_link_description'] = $intro;
421     }
422     if (!empty($instance->id)) {
423         $requestparams['resource_link_id'] = $instance->id;
424     }
425     if (!empty($instance->resource_link_id)) {
426         $requestparams['resource_link_id'] = $instance->resource_link_id;
427     }
428     if ($course->format == 'site') {
429         $requestparams['context_type'] = 'Group';
430     } else {
431         $requestparams['context_type'] = 'CourseSection';
432         $requestparams['lis_course_section_sourcedid'] = $course->idnumber;
433     }
435     if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 ||
436             $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS ||
437             ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))
438     ) {
439         $placementsecret = $instance->servicesalt;
440         $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid));
441         $requestparams['lis_result_sourcedid'] = $sourcedid;
443         // Add outcome service URL.
444         $serviceurl = new \moodle_url('/mod/lti/service.php');
445         $serviceurl = $serviceurl->out();
447         $forcessl = false;
448         if (!empty($CFG->mod_lti_forcessl)) {
449             $forcessl = true;
450         }
452         if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) {
453             $serviceurl = lti_ensure_url_is_https($serviceurl);
454         }
456         $requestparams['lis_outcome_service_url'] = $serviceurl;
457     }
459     // Send user's name and email data if appropriate.
460     if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS ||
461         ($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname)
462             && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS)
463     ) {
464         $requestparams['lis_person_name_given'] = $USER->firstname;
465         $requestparams['lis_person_name_family'] = $USER->lastname;
466         $requestparams['lis_person_name_full'] = fullname($USER);
467         $requestparams['ext_user_username'] = $USER->username;
468     }
470     if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS ||
471         ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr)
472             && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS)
473     ) {
474         $requestparams['lis_person_contact_email_primary'] = $USER->email;
475     }
477     return $requestparams;
480 /**
481  * This function builds the request that must be sent to an LTI 2 tool provider
482  *
483  * @param object    $tool           Basic LTI tool object
484  * @param array     $params         Custom launch parameters
485  *
486  * @return array                    Request details
487  */
488 function lti_build_request_lti2($tool, $params) {
490     $requestparams = array();
492     $capabilities = lti_get_capabilities();
493     $enabledcapabilities = explode("\n", $tool->enabledcapability);
494     foreach ($enabledcapabilities as $capability) {
495         if (array_key_exists($capability, $capabilities)) {
496             $val = $capabilities[$capability];
497             if ($val && (substr($val, 0, 1) != '$')) {
498                 if (isset($params[$val])) {
499                     $requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]];
500                 }
501             }
502         }
503     }
505     return $requestparams;
509 /**
510  * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer
511  *
512  * @param stdClass  $instance       Basic LTI instance object
513  * @param string    $orgid          Organisation ID
514  * @param boolean   $islti2         True if an LTI 2 tool is being launched
515  * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
516  *
517  * @return array                    Request details
518  */
519 function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') {
520     global $CFG;
522     $requestparams = array();
524     if ($instance) {
525         $requestparams['resource_link_id'] = $instance->id;
526         if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) {
527             $requestparams['resource_link_id'] = $instance->resource_link_id;
528         }
529     }
531     $requestparams['launch_presentation_locale'] = current_language();
533     // Make sure we let the tool know what LMS they are being called from.
534     $requestparams['ext_lms'] = 'moodle-2';
535     $requestparams['tool_consumer_info_product_family_code'] = 'moodle';
536     $requestparams['tool_consumer_info_version'] = strval($CFG->version);
538     // Add oauth_callback to be compliant with the 1.0A spec.
539     $requestparams['oauth_callback'] = 'about:blank';
541     if (!$islti2) {
542         $requestparams['lti_version'] = 'LTI-1p0';
543     } else {
544         $requestparams['lti_version'] = 'LTI-2p0';
545     }
546     $requestparams['lti_message_type'] = $messagetype;
548     if ($orgid) {
549         $requestparams["tool_consumer_instance_guid"] = $orgid;
550     }
551     if (!empty($CFG->mod_lti_institution_name)) {
552         $requestparams['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0));
553     } else {
554         $requestparams['tool_consumer_instance_name'] = get_site()->shortname;
555     }
556     $requestparams['tool_consumer_instance_description'] = trim(html_to_text(get_site()->fullname, 0));
558     return $requestparams;
561 /**
562  * This function builds the custom parameters
563  *
564  * @param object    $toolproxy      Tool proxy instance object
565  * @param object    $tool           Tool instance object
566  * @param object    $instance       Tool placement instance object
567  * @param array     $params         LTI launch parameters
568  * @param string    $customstr      Custom parameters defined for tool
569  * @param string    $instructorcustomstr      Custom parameters defined for this placement
570  * @param boolean   $islti2         True if an LTI 2 tool is being launched
571  *
572  * @return array                    Custom parameters
573  */
574 function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) {
576     // Concatenate the custom parameters from the administrator and the instructor
577     // Instructor parameters are only taken into consideration if the administrator
578     // has given permission.
579     $custom = array();
580     if ($customstr) {
581         $custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2);
582     }
583     if ($instructorcustomstr) {
584         $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
585             $instructorcustomstr, $islti2), $custom);
586     }
587     if ($islti2) {
588         $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,
589             $tool->parameter, true), $custom);
590         $settings = lti_get_tool_settings($tool->toolproxyid);
591         $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
592         if (!empty($instance->course)) {
593             $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course);
594             $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
595             if (!empty($instance->id)) {
596                 $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id);
597                 $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));
598             }
599         }
600     }
602     return $custom;
605 /**
606  * Builds a standard LTI Content-Item selection request.
607  *
608  * @param int $id The tool type ID.
609  * @param stdClass $course The course object.
610  * @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP)
611  *                              will use to return the Content-Item message.
612  * @param string $title The tool's title, if available.
613  * @param string $text The text to display to represent the content item. This value may be a long description of the content item.
614  * @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default.
615  * @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened
616  *                                   (via the presentationDocumentTarget element for a returned content item).
617  *                                   If empty, "frame", "iframe", and "window" will be supported by default.
618  * @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without
619  * @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default.
620  *                         any option for the user to cancel the operation. False by default.
621  * @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not.
622  *                       A signed message should always be required when the content item is being created automatically in the
623  *                       TC without further interaction from the user. False by default.
624  * @param bool $canconfirm Flag for can_confirm parameter. False by default.
625  * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default.
626  * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface.
627  * @throws moodle_exception When the LTI tool type does not exist.`
628  * @throws coding_exception For invalid media type and presentation target parameters.
629  */
630 function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
631                                                   $presentationtargets = [], $autocreate = false, $multiple = false,
632                                                   $unsigned = false, $canconfirm = false, $copyadvice = false) {
633     global $USER;
635     $tool = lti_get_type($id);
636     // Validate parameters.
637     if (!$tool) {
638         throw new moodle_exception('errortooltypenotfound', 'mod_lti');
639     }
640     if (!is_array($mediatypes)) {
641         throw new coding_exception('The list of accepted media types should be in an array');
642     }
643     if (!is_array($presentationtargets)) {
644         throw new coding_exception('The list of accepted presentation targets should be in an array');
645     }
647     // Check title. If empty, use the tool's name.
648     if (empty($title)) {
649         $title = $tool->name;
650     }
652     $typeconfig = lti_get_type_config($id);
653     $key = '';
654     $secret = '';
655     $islti2 = false;
656     if (isset($tool->toolproxyid)) {
657         $islti2 = true;
658         $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
659         $key = $toolproxy->guid;
660         $secret = $toolproxy->secret;
661     } else {
662         $toolproxy = null;
663         if (!empty($typeconfig['resourcekey'])) {
664             $key = $typeconfig['resourcekey'];
665         }
666         if (!empty($typeconfig['password'])) {
667             $secret = $typeconfig['password'];
668         }
669     }
670     $tool->enabledcapability = '';
671     if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) {
672         $tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest'];
673     }
675     $tool->parameter = '';
676     if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) {
677         $tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest'];
678     }
680     // Set the tool URL.
681     if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) {
682         $toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']);
683     } else {
684         $toolurl = new moodle_url($typeconfig['toolurl']);
685     }
687     // Check if SSL is forced.
688     if (!empty($typeconfig['forcessl'])) {
689         // Make sure the tool URL is set to https.
690         if (strtolower($toolurl->get_scheme()) === 'http') {
691             $toolurl->set_scheme('https');
692         }
693         // Make sure the return URL is set to https.
694         if (strtolower($returnurl->get_scheme()) === 'http') {
695             $returnurl->set_scheme('https');
696         }
697     }
698     $toolurlout = $toolurl->out(false);
700     // Get base request parameters.
701     $instance = new stdClass();
702     $instance->course = $course->id;
703     $requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2);
705     // Get LTI2-specific request parameters and merge to the request parameters if applicable.
706     if ($islti2) {
707         $lti2params = lti_build_request_lti2($tool, $requestparams);
708         $requestparams = array_merge($requestparams, $lti2params);
709     }
711     // Add the parameters configured by the LTI advantage services.
712     if ($id && !$islti2) {
713         $services = lti_get_services();
714         foreach ($services as $service) {
715             $ltiadvantageparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
716                     $course->id, $USER->id , $id);
717             foreach ($ltiadvantageparameters as $ltiadvantagekey => $ltiadvantagevalue) {
718                 $requestparams[$ltiadvantagekey] = $ltiadvantagevalue;
719             }
720         }
721     }
723     // Get standard request parameters and merge to the request parameters.
724     $orgid = !empty($typeconfig['organizationid']) ? $typeconfig['organizationid'] : '';
725     $standardparams = lti_build_standard_request(null, $orgid, $islti2, 'ContentItemSelectionRequest');
726     $requestparams = array_merge($requestparams, $standardparams);
728     // Get custom request parameters and merge to the request parameters.
729     $customstr = '';
730     if (!empty($typeconfig['customparameters'])) {
731         $customstr = $typeconfig['customparameters'];
732     }
733     $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2);
734     $requestparams = array_merge($requestparams, $customparams);
736     // Allow request params to be updated by sub-plugins.
737     $plugins = core_component::get_plugin_list('ltisource');
738     foreach (array_keys($plugins) as $plugin) {
739         $pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []);
741         if (!empty($pluginparams) && is_array($pluginparams)) {
742             $requestparams = array_merge($requestparams, $pluginparams);
743         }
744     }
746     // Media types. Set to ltilink by default if empty.
747     if (empty($mediatypes)) {
748         $mediatypes = [
749             'application/vnd.ims.lti.v1.ltilink',
750         ];
751     }
752     $requestparams['accept_media_types'] = implode(',', $mediatypes);
754     // Presentation targets. Supports frame, iframe, window by default if empty.
755     if (empty($presentationtargets)) {
756         $presentationtargets = [
757             'frame',
758             'iframe',
759             'window',
760         ];
761     }
762     $requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets);
764     // Other request parameters.
765     $requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false';
766     $requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false';
767     $requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false';
768     $requestparams['auto_create'] = $autocreate === true ? 'true' : 'false';
769     $requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false';
770     $requestparams['content_item_return_url'] = $returnurl->out(false);
771     $requestparams['title'] = $title;
772     $requestparams['text'] = $text;
773     $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret);
774     $toolurlparams = $toolurl->params();
776     // Strip querystring params in endpoint url from $signedparams to avoid duplication.
777     if (!empty($toolurlparams) && !empty($signedparams)) {
778         foreach (array_keys($toolurlparams) as $paramname) {
779             if (isset($signedparams[$paramname])) {
780                 unset($signedparams[$paramname]);
781             }
782         }
783     }
785     // Check for params that should not be passed. Unset if they are set.
786     $unwantedparams = [
787         'resource_link_id',
788         'resource_link_title',
789         'resource_link_description',
790         'launch_presentation_return_url',
791         'lis_result_sourcedid',
792     ];
793     foreach ($unwantedparams as $param) {
794         if (isset($signedparams[$param])) {
795             unset($signedparams[$param]);
796         }
797     }
799     // Prepare result object.
800     $result = new stdClass();
801     $result->params = $signedparams;
802     $result->url = $toolurlout;
804     return $result;
807 /**
808  * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
809  * selected content item. This configuration data can be then used when adding a tool into the course.
810  *
811  * @param int $typeid The tool type ID.
812  * @param string $messagetype The value for the lti_message_type parameter.
813  * @param string $ltiversion The value for the lti_version parameter.
814  * @param string $consumerkey The consumer key.
815  * @param string $contentitemsjson The JSON string for the content_items parameter.
816  * @return stdClass The array of module information objects.
817  * @throws moodle_exception
818  * @throws lti\OAuthException
819  */
820 function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
821     $tool = lti_get_type($typeid);
822     // Validate parameters.
823     if (!$tool) {
824         throw new moodle_exception('errortooltypenotfound', 'mod_lti');
825     }
826     // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
827     // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
828     if ($messagetype !== 'ContentItemSelection') {
829         debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
830             DEBUG_DEVELOPER);
831     }
833     $typeconfig = lti_get_type_config($typeid);
835     if (isset($tool->toolproxyid)) {
836         $islti2 = true;
837         $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
838         $key = $toolproxy->guid;
839         $secret = $toolproxy->secret;
840     } else {
841         $islti2 = false;
842         $toolproxy = null;
843         if (!empty($typeconfig['resourcekey'])) {
844             $key = $typeconfig['resourcekey'];
845         } else {
846             $key = '';
847         }
848         if (!empty($typeconfig['password'])) {
849             $secret = $typeconfig['password'];
850         } else {
851             $secret = '';
852         }
853     }
855     // Check LTI versions from our side and the response's side. Show debugging if they don't match.
856     // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
857     $expectedversion = LTI_VERSION_1;
858     if ($islti2) {
859         $expectedversion = LTI_VERSION_2;
860     }
861     if ($ltiversion !== $expectedversion) {
862         debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
863             " Response: {$ltiversion}", DEBUG_DEVELOPER);
864     }
866     if ($consumerkey !== $key) {
867         throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
868     }
870     $store = new lti\TrivialOAuthDataStore();
871     $store->add_consumer($key, $secret);
872     $server = new lti\OAuthServer($store);
873     $method = new lti\OAuthSignatureMethod_HMAC_SHA1();
874     $server->add_signature_method($method);
875     $request = lti\OAuthRequest::from_request();
876     try {
877         $server->verify_request($request);
878     } catch (lti\OAuthException $e) {
879         throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage());
880     }
882     $items = json_decode($contentitemsjson);
883     if (empty($items)) {
884         throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
885     }
886     if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'}) || (count($items->{'@graph'}) > 1)) {
887         throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
888     }
890     $config = null;
891     if (!empty($items->{'@graph'})) {
892         $item = $items->{'@graph'}[0];
894         $config = new stdClass();
895         $config->name = '';
896         if (isset($item->title)) {
897             $config->name = $item->title;
898         }
899         if (empty($config->name)) {
900             $config->name = $tool->name;
901         }
902         if (isset($item->text)) {
903             $config->introeditor = [
904                 'text' => $item->text,
905                 'format' => FORMAT_PLAIN
906             ];
907         }
908         if (isset($item->icon->{'@id'})) {
909             $iconurl = new moodle_url($item->icon->{'@id'});
910             // Assign item's icon URL to secureicon or icon depending on its scheme.
911             if (strtolower($iconurl->get_scheme()) === 'https') {
912                 $config->secureicon = $iconurl->out(false);
913             } else {
914                 $config->icon = $iconurl->out(false);
915             }
916         }
917         if (isset($item->url)) {
918             $url = new moodle_url($item->url);
919             $config->toolurl = $url->out(false);
920             $config->typeid = 0;
921         } else {
922             $config->typeid = $typeid;
923         }
924         $config->instructorchoicesendname = LTI_SETTING_NEVER;
925         $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
926         $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
927         $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
928         if (isset($item->placementAdvice->presentationDocumentTarget)) {
929             if ($item->placementAdvice->presentationDocumentTarget === 'window') {
930                 $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW;
931             } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') {
932                 $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
933             } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') {
934                 $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED;
935             }
936         }
937         if (isset($item->custom)) {
938             $customparameters = [];
939             foreach ($item->custom as $key => $value) {
940                 $customparameters[] = "{$key}={$value}";
941             }
942             $config->instructorcustomparameters = implode("\n", $customparameters);
943         }
944     }
945     return $config;
948 function lti_get_tool_table($tools, $id) {
949     global $OUTPUT;
950     $html = '';
952     $typename = get_string('typename', 'lti');
953     $baseurl = get_string('baseurl', 'lti');
954     $action = get_string('action', 'lti');
955     $createdon = get_string('createdon', 'lti');
957     if (!empty($tools)) {
958         $html .= "
959         <div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\">
960             <table id=\"{$id}_tools\">
961                 <thead>
962                     <tr>
963                         <th>$typename</th>
964                         <th>$baseurl</th>
965                         <th>$createdon</th>
966                         <th>$action</th>
967                     </tr>
968                 </thead>
969         ";
971         foreach ($tools as $type) {
972             $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
973             $accept = get_string('accept', 'lti');
974             $update = get_string('update', 'lti');
975             $delete = get_string('delete', 'lti');
977             if (empty($type->toolproxyid)) {
978                 $baseurl = new \moodle_url('/mod/lti/typessettings.php', array(
979                         'action' => 'accept',
980                         'id' => $type->id,
981                         'sesskey' => sesskey(),
982                         'tab' => $id
983                     ));
984                 $ref = $type->baseurl;
985             } else {
986                 $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array(
987                         'action' => 'accept',
988                         'id' => $type->id,
989                         'sesskey' => sesskey(),
990                         'tab' => $id
991                     ));
992                 $ref = $type->tpname;
993             }
995             $accepthtml = $OUTPUT->action_icon($baseurl,
996                     new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
997                     array('title' => $accept, 'class' => 'editing_accept'));
999             $deleteaction = 'delete';
1001             if ($type->state == LTI_TOOL_STATE_CONFIGURED) {
1002                 $accepthtml = '';
1003             }
1005             if ($type->state != LTI_TOOL_STATE_REJECTED) {
1006                 $deleteaction = 'reject';
1007                 $delete = get_string('reject', 'lti');
1008             }
1010             $updateurl = clone($baseurl);
1011             $updateurl->param('action', 'update');
1012             $updatehtml = $OUTPUT->action_icon($updateurl,
1013                     new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1014                     array('title' => $update, 'class' => 'editing_update'));
1016             if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) {
1017                 $deleteurl = clone($baseurl);
1018                 $deleteurl->param('action', $deleteaction);
1019                 $deletehtml = $OUTPUT->action_icon($deleteurl,
1020                         new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1021                         array('title' => $delete, 'class' => 'editing_delete'));
1022             } else {
1023                 $deletehtml = '';
1024             }
1025             $html .= "
1026             <tr>
1027                 <td>
1028                     {$type->name}
1029                 </td>
1030                 <td>
1031                     {$ref}
1032                 </td>
1033                 <td>
1034                     {$date}
1035                 </td>
1036                 <td align=\"center\">
1037                     {$accepthtml}{$updatehtml}{$deletehtml}
1038                 </td>
1039             </tr>
1040             ";
1041         }
1042         $html .= '</table></div>';
1043     } else {
1044         $html .= get_string('no_' . $id, 'lti');
1045     }
1047     return $html;
1050 /**
1051  * This function builds the tab for a category of tool proxies
1052  *
1053  * @param object    $toolproxies    Tool proxy instance objects
1054  * @param string    $id             Category ID
1055  *
1056  * @return string                   HTML for tab
1057  */
1058 function lti_get_tool_proxy_table($toolproxies, $id) {
1059     global $OUTPUT;
1061     if (!empty($toolproxies)) {
1062         $typename = get_string('typename', 'lti');
1063         $url = get_string('registrationurl', 'lti');
1064         $action = get_string('action', 'lti');
1065         $createdon = get_string('createdon', 'lti');
1067         $html = <<< EOD
1068         <div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em">
1069             <table id="{$id}_tool_proxies">
1070                 <thead>
1071                     <tr>
1072                         <th>{$typename}</th>
1073                         <th>{$url}</th>
1074                         <th>{$createdon}</th>
1075                         <th>{$action}</th>
1076                     </tr>
1077                 </thead>
1078 EOD;
1079         foreach ($toolproxies as $toolproxy) {
1080             $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));
1081             $accept = get_string('register', 'lti');
1082             $update = get_string('update', 'lti');
1083             $delete = get_string('delete', 'lti');
1085             $baseurl = new \moodle_url('/mod/lti/registersettings.php', array(
1086                     'action' => 'accept',
1087                     'id' => $toolproxy->id,
1088                     'sesskey' => sesskey(),
1089                     'tab' => $id
1090                 ));
1092             $registerurl = new \moodle_url('/mod/lti/register.php', array(
1093                     'id' => $toolproxy->id,
1094                     'sesskey' => sesskey(),
1095                     'tab' => 'tool_proxy'
1096                 ));
1098             $accepthtml = $OUTPUT->action_icon($registerurl,
1099                     new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,
1100                     array('title' => $accept, 'class' => 'editing_accept'));
1102             $deleteaction = 'delete';
1104             if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) {
1105                 $accepthtml = '';
1106             }
1108             if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) {
1109                 $delete = get_string('cancel', 'lti');
1110             }
1112             $updateurl = clone($baseurl);
1113             $updateurl->param('action', 'update');
1114             $updatehtml = $OUTPUT->action_icon($updateurl,
1115                     new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,
1116                     array('title' => $update, 'class' => 'editing_update'));
1118             $deleteurl = clone($baseurl);
1119             $deleteurl->param('action', $deleteaction);
1120             $deletehtml = $OUTPUT->action_icon($deleteurl,
1121                     new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,
1122                     array('title' => $delete, 'class' => 'editing_delete'));
1123             $html .= <<< EOD
1124             <tr>
1125                 <td>
1126                     {$toolproxy->name}
1127                 </td>
1128                 <td>
1129                     {$toolproxy->regurl}
1130                 </td>
1131                 <td>
1132                     {$date}
1133                 </td>
1134                 <td align="center">
1135                     {$accepthtml}{$updatehtml}{$deletehtml}
1136                 </td>
1137             </tr>
1138 EOD;
1139         }
1140         $html .= '</table></div>';
1141     } else {
1142         $html = get_string('no_' . $id, 'lti');
1143     }
1145     return $html;
1148 /**
1149  * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter
1150  *
1151  * @param object $tool  Tool instance object
1152  *
1153  * @return array List of enabled capabilities
1154  */
1155 function lti_get_enabled_capabilities($tool) {
1156     if (!isset($tool)) {
1157         return array();
1158     }
1159     if (!empty($tool->enabledcapability)) {
1160         $enabledcapabilities = explode("\n", $tool->enabledcapability);
1161     } else {
1162         $enabledcapabilities = array();
1163     }
1164     $paramstr = str_replace("\r\n", "\n", $tool->parameter);
1165     $paramstr = str_replace("\n\r", "\n", $paramstr);
1166     $paramstr = str_replace("\r", "\n", $paramstr);
1167     $params = explode("\n", $paramstr);
1168     foreach ($params as $param) {
1169         $pos = strpos($param, '=');
1170         if (($pos === false) || ($pos < 1)) {
1171             continue;
1172         }
1173         $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
1174         if (substr($value, 0, 1) == '$') {
1175             $value = substr($value, 1);
1176             if (!in_array($value, $enabledcapabilities)) {
1177                 $enabledcapabilities[] = $value;
1178             }
1179         }
1180     }
1181     return $enabledcapabilities;
1184 /**
1185  * Splits the custom parameters field to the various parameters
1186  *
1187  * @param object    $toolproxy      Tool proxy instance object
1188  * @param object    $tool           Tool instance object
1189  * @param array     $params         LTI launch parameters
1190  * @param string    $customstr      String containing the parameters
1191  * @param boolean   $islti2         True if an LTI 2 tool is being launched
1192  *
1193  * @return array of custom parameters
1194  */
1195 function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {
1196     $customstr = str_replace("\r\n", "\n", $customstr);
1197     $customstr = str_replace("\n\r", "\n", $customstr);
1198     $customstr = str_replace("\r", "\n", $customstr);
1199     $lines = explode("\n", $customstr);  // Or should this split on "/[\n;]/"?
1200     $retval = array();
1201     foreach ($lines as $line) {
1202         $pos = strpos($line, '=');
1203         if ( $pos === false || $pos < 1 ) {
1204             continue;
1205         }
1206         $key = trim(core_text::substr($line, 0, $pos));
1207         $key = lti_map_keyname($key, false);
1208         $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
1209         $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
1210         $key2 = lti_map_keyname($key);
1211         $retval['custom_'.$key2] = $val;
1212         if ($key != $key2) {
1213             $retval['custom_'.$key] = $val;
1214         }
1215     }
1216     return $retval;
1219 /**
1220  * Adds the custom parameters to an array
1221  *
1222  * @param object    $toolproxy      Tool proxy instance object
1223  * @param object    $tool           Tool instance object
1224  * @param array     $params         LTI launch parameters
1225  * @param array     $parameters     Array containing the parameters
1226  *
1227  * @return array    Array of custom parameters
1228  */
1229 function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {
1230     $retval = array();
1231     foreach ($parameters as $key => $val) {
1232         $key2 = lti_map_keyname($key);
1233         $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true);
1234         $retval['custom_'.$key2] = $val;
1235         if ($key != $key2) {
1236             $retval['custom_'.$key] = $val;
1237         }
1238     }
1239     return $retval;
1242 /**
1243  * Parse a custom parameter to replace any substitution variables
1244  *
1245  * @param object    $toolproxy      Tool proxy instance object
1246  * @param object    $tool           Tool instance object
1247  * @param array     $params         LTI launch parameters
1248  * @param string    $value          Custom parameter value
1249  * @param boolean   $islti2         True if an LTI 2 tool is being launched
1250  *
1251  * @return string Parsed value of custom parameter
1252  */
1253 function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {
1254     // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER var.
1255     global $USER;
1257     if ($value) {
1258         if (substr($value, 0, 1) == '\\') {
1259             $value = substr($value, 1);
1260         } else if (substr($value, 0, 1) == '$') {
1261             $value1 = substr($value, 1);
1262             $enabledcapabilities = lti_get_enabled_capabilities($tool);
1263             if (!$islti2 || in_array($value1, $enabledcapabilities)) {
1264                 $capabilities = lti_get_capabilities();
1265                 if (array_key_exists($value1, $capabilities)) {
1266                     $val = $capabilities[$value1];
1267                     if ($val) {
1268                         if (substr($val, 0, 1) != '$') {
1269                             $value = $params[$val];
1270                         } else {
1271                             $valarr = explode('->', substr($val, 1), 2);
1272                             $value = "{${$valarr[0]}->{$valarr[1]}}";
1273                             $value = str_replace('<br />' , ' ', $value);
1274                             $value = str_replace('<br>' , ' ', $value);
1275                             $value = format_string($value);
1276                         }
1277                     } else {
1278                         $value = lti_calculate_custom_parameter($value1);
1279                     }
1280                 } else if ($islti2) {
1281                     $val = $value;
1282                     $services = lti_get_services();
1283                     foreach ($services as $service) {
1284                         $service->set_tool_proxy($toolproxy);
1285                         $value = $service->parse_value($val);
1286                         if ($val != $value) {
1287                             break;
1288                         }
1289                     }
1290                 }
1291             }
1292         }
1293     }
1294     return $value;
1297 /**
1298  * Calculates the value of a custom parameter that has not been specified earlier
1299  *
1300  * @param string    $value          Custom parameter value
1301  *
1302  * @return string Calculated value of custom parameter
1303  */
1304 function lti_calculate_custom_parameter($value) {
1305     global $USER, $COURSE;
1307     switch ($value) {
1308         case 'Moodle.Person.userGroupIds':
1309             return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]);
1310     }
1311     return null;
1314 /**
1315  * Used for building the names of the different custom parameters
1316  *
1317  * @param string $key   Parameter name
1318  * @param bool $tolower Do we want to convert the key into lower case?
1319  * @return string       Processed name
1320  */
1321 function lti_map_keyname($key, $tolower = true) {
1322     $newkey = "";
1323     if ($tolower) {
1324         $key = core_text::strtolower(trim($key));
1325     }
1326     foreach (str_split($key) as $ch) {
1327         if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') || (!$tolower && ($ch >= 'A' && $ch <= 'Z'))) {
1328             $newkey .= $ch;
1329         } else {
1330             $newkey .= '_';
1331         }
1332     }
1333     return $newkey;
1336 /**
1337  * Gets the IMS role string for the specified user and LTI course module.
1338  *
1339  * @param mixed    $user      User object or user id
1340  * @param int      $cmid      The course module id of the LTI activity
1341  * @param int      $courseid  The course id of the LTI activity
1342  * @param boolean  $islti2    True if an LTI 2 tool is being launched
1343  *
1344  * @return string A role string suitable for passing with an LTI launch
1345  */
1346 function lti_get_ims_role($user, $cmid, $courseid, $islti2) {
1347     $roles = array();
1349     if (empty($cmid)) {
1350         // If no cmid is passed, check if the user is a teacher in the course
1351         // This allows other modules to programmatically "fake" a launch without
1352         // a real LTI instance.
1353         $context = context_course::instance($courseid);
1355         if (has_capability('moodle/course:manageactivities', $context, $user)) {
1356             array_push($roles, 'Instructor');
1357         } else {
1358             array_push($roles, 'Learner');
1359         }
1360     } else {
1361         $context = context_module::instance($cmid);
1363         if (has_capability('mod/lti:manage', $context)) {
1364             array_push($roles, 'Instructor');
1365         } else {
1366             array_push($roles, 'Learner');
1367         }
1368     }
1370     if (is_siteadmin($user) || has_capability('mod/lti:admin', $context)) {
1371         // Make sure admins do not have the Learner role, then set admin role.
1372         $roles = array_diff($roles, array('Learner'));
1373         if (!$islti2) {
1374             array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
1375         } else {
1376             array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator');
1377         }
1378     }
1380     return join(',', $roles);
1383 /**
1384  * Returns configuration details for the tool
1385  *
1386  * @param int $typeid   Basic LTI tool typeid
1387  *
1388  * @return array        Tool Configuration
1389  */
1390 function lti_get_type_config($typeid) {
1391     global $DB;
1393     $query = "SELECT name, value
1394                 FROM {lti_types_config}
1395                WHERE typeid = :typeid1
1396            UNION ALL
1397               SELECT 'toolurl' AS name, baseurl AS value
1398                 FROM {lti_types}
1399                WHERE id = :typeid2
1400            UNION ALL
1401               SELECT 'icon' AS name, icon AS value
1402                 FROM {lti_types}
1403                WHERE id = :typeid3
1404            UNION ALL
1405               SELECT 'secureicon' AS name, secureicon AS value
1406                 FROM {lti_types}
1407                WHERE id = :typeid4";
1409     $typeconfig = array();
1410     $configs = $DB->get_records_sql($query,
1411         array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid));
1413     if (!empty($configs)) {
1414         foreach ($configs as $config) {
1415             $typeconfig[$config->name] = $config->value;
1416         }
1417     }
1419     return $typeconfig;
1422 function lti_get_tools_by_url($url, $state, $courseid = null) {
1423     $domain = lti_get_domain_from_url($url);
1425     return lti_get_tools_by_domain($domain, $state, $courseid);
1428 function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
1429     global $DB, $SITE;
1431     $statefilter = '';
1432     $coursefilter = '';
1434     if ($state) {
1435         $statefilter = 'AND state = :state';
1436     }
1438     if ($courseid && $courseid != $SITE->id) {
1439         $coursefilter = 'OR course = :courseid';
1440     }
1442     $query = "SELECT *
1443                 FROM {lti_types}
1444                WHERE tooldomain = :tooldomain
1445                  AND (course = :siteid $coursefilter)
1446                  $statefilter";
1448     return $DB->get_records_sql($query, array(
1449         'courseid' => $courseid,
1450         'siteid' => $SITE->id,
1451         'tooldomain' => $domain,
1452         'state' => $state
1453     ));
1456 /**
1457  * Returns all basicLTI tools configured by the administrator
1458  *
1459  * @param int $course
1460  *
1461  * @return array
1462  */
1463 function lti_filter_get_types($course) {
1464     global $DB;
1466     if (!empty($course)) {
1467         $where = "WHERE t.course = :course";
1468         $params = array('course' => $course);
1469     } else {
1470         $where = '';
1471         $params = array();
1472     }
1473     $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname
1474                 FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id
1475                 {$where}";
1476     return $DB->get_records_sql($query, $params);
1479 /**
1480  * Given an array of tools, filter them based on their state
1481  *
1482  * @param array $tools An array of lti_types records
1483  * @param int $state One of the LTI_TOOL_STATE_* constants
1484  * @return array
1485  */
1486 function lti_filter_tool_types(array $tools, $state) {
1487     $return = array();
1488     foreach ($tools as $key => $tool) {
1489         if ($tool->state == $state) {
1490             $return[$key] = $tool;
1491         }
1492     }
1493     return $return;
1496 /**
1497  * Returns all lti types visible in this course
1498  *
1499  * @param int $courseid The id of the course to retieve types for
1500  * @param array $coursevisible options for 'coursevisible' field,
1501  *        default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]
1502  * @return stdClass[] All the lti types visible in the given course
1503  */
1504 function lti_get_lti_types_by_course($courseid, $coursevisible = null) {
1505     global $DB, $SITE;
1507     if ($coursevisible === null) {
1508         $coursevisible = [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER];
1509     }
1511     list($coursevisiblesql, $coursevisparams) = $DB->get_in_or_equal($coursevisible, SQL_PARAMS_NAMED, 'coursevisible');
1512     $query = "SELECT *
1513                 FROM {lti_types}
1514                WHERE coursevisible $coursevisiblesql
1515                  AND (course = :siteid OR course = :courseid)
1516                  AND state = :active";
1518     return $DB->get_records_sql($query,
1519         array('siteid' => $SITE->id, 'courseid' => $courseid, 'active' => LTI_TOOL_STATE_CONFIGURED) + $coursevisparams);
1522 /**
1523  * Returns tool types for lti add instance and edit page
1524  *
1525  * @return array Array of lti types
1526  */
1527 function lti_get_types_for_add_instance() {
1528     global $COURSE;
1529     $admintypes = lti_get_lti_types_by_course($COURSE->id);
1531     $types = array();
1532     $types[0] = (object)array('name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null);
1534     foreach ($admintypes as $type) {
1535         $types[$type->id] = $type;
1536     }
1538     return $types;
1541 /**
1542  * Returns a list of configured types in the given course
1543  *
1544  * @param int $courseid The id of the course to retieve types for
1545  * @param int $sectionreturn section to return to for forming the URLs
1546  * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link
1547  */
1548 function lti_get_configured_types($courseid, $sectionreturn = 0) {
1549     global $OUTPUT;
1550     $types = array();
1551     $admintypes = lti_get_lti_types_by_course($courseid, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]);
1553     foreach ($admintypes as $ltitype) {
1554         $type           = new stdClass();
1555         $type->modclass = MOD_CLASS_ACTIVITY;
1556         $type->name     = 'lti_type_' . $ltitype->id;
1557         // Clean the name. We don't want tags here.
1558         $type->title    = clean_param($ltitype->name, PARAM_NOTAGS);
1559         $trimmeddescription = trim($ltitype->description);
1560         if ($trimmeddescription != '') {
1561             // Clean the description. We don't want tags here.
1562             $type->help     = clean_param($trimmeddescription, PARAM_NOTAGS);
1563             $type->helplink = get_string('modulename_shortcut_link', 'lti');
1564         }
1565         if (empty($ltitype->icon)) {
1566             $type->icon = $OUTPUT->pix_icon('icon', '', 'lti', array('class' => 'icon'));
1567         } else {
1568             $type->icon = html_writer::empty_tag('img', array('src' => $ltitype->icon, 'alt' => $ltitype->name, 'class' => 'icon'));
1569         }
1570         $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'course' => $courseid,
1571             'sr' => $sectionreturn, 'typeid' => $ltitype->id));
1572         $types[] = $type;
1573     }
1574     return $types;
1577 function lti_get_domain_from_url($url) {
1578     $matches = array();
1580     if (preg_match(LTI_URL_DOMAIN_REGEX, $url, $matches)) {
1581         return $matches[1];
1582     }
1585 function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) {
1586     $possibletools = lti_get_tools_by_url($url, $state, $courseid);
1588     return lti_get_best_tool_by_url($url, $possibletools, $courseid);
1591 function lti_get_url_thumbprint($url) {
1592     // Parse URL requires a schema otherwise everything goes into 'path'.  Fixed 5.4.7 or later.
1593     if (preg_match('/https?:\/\//', $url) !== 1) {
1594         $url = 'http://'.$url;
1595     }
1596     $urlparts = parse_url(strtolower($url));
1597     if (!isset($urlparts['path'])) {
1598         $urlparts['path'] = '';
1599     }
1601     if (!isset($urlparts['query'])) {
1602         $urlparts['query'] = '';
1603     }
1605     if (!isset($urlparts['host'])) {
1606         $urlparts['host'] = '';
1607     }
1609     if (substr($urlparts['host'], 0, 4) === 'www.') {
1610         $urlparts['host'] = substr($urlparts['host'], 4);
1611     }
1613     $urllower = $urlparts['host'] . '/' . $urlparts['path'];
1615     if ($urlparts['query'] != '') {
1616         $urllower .= '?' . $urlparts['query'];
1617     }
1619     return $urllower;
1622 function lti_get_best_tool_by_url($url, $tools, $courseid = null) {
1623     if (count($tools) === 0) {
1624         return null;
1625     }
1627     $urllower = lti_get_url_thumbprint($url);
1629     foreach ($tools as $tool) {
1630         $tool->_matchscore = 0;
1632         $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl);
1634         if ($urllower === $toolbaseurllower) {
1635             // 100 points for exact thumbprint match.
1636             $tool->_matchscore += 100;
1637         } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) {
1638             // 50 points if tool thumbprint starts with the base URL thumbprint.
1639             $tool->_matchscore += 50;
1640         }
1642         // Prefer course tools over site tools.
1643         if (!empty($courseid)) {
1644             // Minus 10 points for not matching the course id (global tools).
1645             if ($tool->course != $courseid) {
1646                 $tool->_matchscore -= 10;
1647             }
1648         }
1649     }
1651     $bestmatch = array_reduce($tools, function($value, $tool) {
1652         if ($tool->_matchscore > $value->_matchscore) {
1653             return $tool;
1654         } else {
1655             return $value;
1656         }
1658     }, (object)array('_matchscore' => -1));
1660     // None of the tools are suitable for this URL.
1661     if ($bestmatch->_matchscore <= 0) {
1662         return null;
1663     }
1665     return $bestmatch;
1668 function lti_get_shared_secrets_by_key($key) {
1669     global $DB;
1671     // Look up the shared secret for the specified key in both the types_config table (for configured tools)
1672     // And in the lti resource table for ad-hoc tools.
1673     $query = "SELECT t2.value
1674                 FROM {lti_types_config} t1
1675                 JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
1676                 JOIN {lti_types} type ON t2.typeid = type.id
1677               WHERE t1.name = 'resourcekey'
1678                 AND t1.value = :key1
1679                 AND t2.name = 'password'
1680                 AND type.state = :configured1
1681                UNION
1682               SELECT tp.secret AS value
1683                 FROM {lti_tool_proxies} tp
1684                 JOIN {lti_types} t ON tp.id = t.toolproxyid
1685               WHERE tp.guid = :key2
1686                 AND t.state = :configured2
1687               UNION
1688              SELECT password AS value
1689                FROM {lti}
1690               WHERE resourcekey = :key3";
1692     $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED,
1693         'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));
1695     $values = array_map(function($item) {
1696         return $item->value;
1697     }, $sharedsecrets);
1699     // There should really only be one shared secret per key. But, we can't prevent
1700     // more than one getting entered. For instance, if the same key is used for two tool providers.
1701     return $values;
1704 /**
1705  * Delete a Basic LTI configuration
1706  *
1707  * @param int $id   Configuration id
1708  */
1709 function lti_delete_type($id) {
1710     global $DB;
1712     // We should probably just copy the launch URL to the tool instances in this case... using a single query.
1713     /*
1714     $instances = $DB->get_records('lti', array('typeid' => $id));
1715     foreach ($instances as $instance) {
1716         $instance->typeid = 0;
1717         $DB->update_record('lti', $instance);
1718     }*/
1720     $DB->delete_records('lti_types', array('id' => $id));
1721     $DB->delete_records('lti_types_config', array('typeid' => $id));
1724 function lti_set_state_for_type($id, $state) {
1725     global $DB;
1727     $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));
1730 /**
1731  * Transforms a basic LTI object to an array
1732  *
1733  * @param object $ltiobject    Basic LTI object
1734  *
1735  * @return array Basic LTI configuration details
1736  */
1737 function lti_get_config($ltiobject) {
1738     $typeconfig = (array)$ltiobject;
1739     $additionalconfig = lti_get_type_config($ltiobject->typeid);
1740     $typeconfig = array_merge($typeconfig, $additionalconfig);
1741     return $typeconfig;
1744 /**
1745  *
1746  * Generates some of the tool configuration based on the instance details
1747  *
1748  * @param int $id
1749  *
1750  * @return object configuration
1751  *
1752  */
1753 function lti_get_type_config_from_instance($id) {
1754     global $DB;
1756     $instance = $DB->get_record('lti', array('id' => $id));
1757     $config = lti_get_config($instance);
1759     $type = new \stdClass();
1760     $type->lti_fix = $id;
1761     if (isset($config['toolurl'])) {
1762         $type->lti_toolurl = $config['toolurl'];
1763     }
1764     if (isset($config['instructorchoicesendname'])) {
1765         $type->lti_sendname = $config['instructorchoicesendname'];
1766     }
1767     if (isset($config['instructorchoicesendemailaddr'])) {
1768         $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr'];
1769     }
1770     if (isset($config['instructorchoiceacceptgrades'])) {
1771         $type->lti_acceptgrades = $config['instructorchoiceacceptgrades'];
1772     }
1773     if (isset($config['instructorchoiceallowroster'])) {
1774         $type->lti_allowroster = $config['instructorchoiceallowroster'];
1775     }
1777     if (isset($config['instructorcustomparameters'])) {
1778         $type->lti_allowsetting = $config['instructorcustomparameters'];
1779     }
1780     return $type;
1783 /**
1784  * Generates some of the tool configuration based on the admin configuration details
1785  *
1786  * @param int $id
1787  *
1788  * @return stdClass Configuration details
1789  */
1790 function lti_get_type_type_config($id) {
1791     global $DB;
1793     $basicltitype = $DB->get_record('lti_types', array('id' => $id));
1794     $config = lti_get_type_config($id);
1796     $type = new \stdClass();
1798     $type->lti_typename = $basicltitype->name;
1800     $type->typeid = $basicltitype->id;
1802     $type->toolproxyid = $basicltitype->toolproxyid;
1804     $type->lti_toolurl = $basicltitype->baseurl;
1806     $type->lti_description = $basicltitype->description;
1808     $type->lti_parameters = $basicltitype->parameter;
1810     $type->lti_icon = $basicltitype->icon;
1812     $type->lti_secureicon = $basicltitype->secureicon;
1814     if (isset($config['resourcekey'])) {
1815         $type->lti_resourcekey = $config['resourcekey'];
1816     }
1817     if (isset($config['password'])) {
1818         $type->lti_password = $config['password'];
1819     }
1821     if (isset($config['sendname'])) {
1822         $type->lti_sendname = $config['sendname'];
1823     }
1824     if (isset($config['instructorchoicesendname'])) {
1825         $type->lti_instructorchoicesendname = $config['instructorchoicesendname'];
1826     }
1827     if (isset($config['sendemailaddr'])) {
1828         $type->lti_sendemailaddr = $config['sendemailaddr'];
1829     }
1830     if (isset($config['instructorchoicesendemailaddr'])) {
1831         $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr'];
1832     }
1833     if (isset($config['acceptgrades'])) {
1834         $type->lti_acceptgrades = $config['acceptgrades'];
1835     }
1836     if (isset($config['instructorchoiceacceptgrades'])) {
1837         $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades'];
1838     }
1839     if (isset($config['allowroster'])) {
1840         $type->lti_allowroster = $config['allowroster'];
1841     }
1842     if (isset($config['instructorchoiceallowroster'])) {
1843         $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster'];
1844     }
1846     if (isset($config['customparameters'])) {
1847         $type->lti_customparameters = $config['customparameters'];
1848     }
1850     if (isset($config['forcessl'])) {
1851         $type->lti_forcessl = $config['forcessl'];
1852     }
1854     if (isset($config['organizationid'])) {
1855         $type->lti_organizationid = $config['organizationid'];
1856     }
1857     if (isset($config['organizationurl'])) {
1858         $type->lti_organizationurl = $config['organizationurl'];
1859     }
1860     if (isset($config['organizationdescr'])) {
1861         $type->lti_organizationdescr = $config['organizationdescr'];
1862     }
1863     if (isset($config['launchcontainer'])) {
1864         $type->lti_launchcontainer = $config['launchcontainer'];
1865     }
1867     if (isset($config['coursevisible'])) {
1868         $type->lti_coursevisible = $config['coursevisible'];
1869     }
1871     if (isset($config['contentitem'])) {
1872         $type->lti_contentitem = $config['contentitem'];
1873     }
1875     if (isset($config['toolurl_ContentItemSelectionRequest'])) {
1876         $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];
1877     }
1879     if (isset($config['debuglaunch'])) {
1880         $type->lti_debuglaunch = $config['debuglaunch'];
1881     }
1883     if (isset($config['module_class_type'])) {
1884         $type->lti_module_class_type = $config['module_class_type'];
1885     }
1887     // Get the parameters from the LTI services.
1888     $services = lti_get_services();
1889     $ltiserviceprefixlength = 11;
1890     foreach ($services as $service) {
1891         $configurationparameters = $service->get_configuration_parameter_names();
1892         foreach ($configurationparameters as $ltiserviceparameter) {
1893             $shortltiserviceparameter = substr($ltiserviceparameter, $ltiserviceprefixlength);
1894             if (isset($config[$shortltiserviceparameter])) {
1895                 $type->$ltiserviceparameter = $config[$shortltiserviceparameter];
1896             }
1897         }
1898     }
1900     return $type;
1903 function lti_prepare_type_for_save($type, $config) {
1904     if (isset($config->lti_toolurl)) {
1905         $type->baseurl = $config->lti_toolurl;
1906         $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
1907     }
1908     if (isset($config->lti_description)) {
1909         $type->description = $config->lti_description;
1910     }
1911     if (isset($config->lti_typename)) {
1912         $type->name = $config->lti_typename;
1913     }
1914     if (isset($config->lti_coursevisible)) {
1915         $type->coursevisible = $config->lti_coursevisible;
1916     }
1918     if (isset($config->lti_icon)) {
1919         $type->icon = $config->lti_icon;
1920     }
1921     if (isset($config->lti_secureicon)) {
1922         $type->secureicon = $config->lti_secureicon;
1923     }
1925     $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0;
1926     $config->lti_forcessl = $type->forcessl;
1927     if (isset($config->lti_contentitem)) {
1928         $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;
1929         $config->lti_contentitem = $type->contentitem;
1930     }
1931     if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
1932         if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {
1933             $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;
1934         } else {
1935             $type->toolurl_ContentItemSelectionRequest = '';
1936         }
1937         $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;
1938     }
1940     $type->timemodified = time();
1942     unset ($config->lti_typename);
1943     unset ($config->lti_toolurl);
1944     unset ($config->lti_description);
1945     unset ($config->lti_icon);
1946     unset ($config->lti_secureicon);
1949 function lti_update_type($type, $config) {
1950     global $DB, $CFG;
1952     lti_prepare_type_for_save($type, $config);
1954     if (lti_request_is_using_ssl() && !empty($type->secureicon)) {
1955         $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);
1956     } else {
1957         $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon));
1958     }
1959     unset($config->oldicon);
1961     if ($DB->update_record('lti_types', $type)) {
1962         foreach ($config as $key => $value) {
1963             if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {
1964                 $record = new \StdClass();
1965                 $record->typeid = $type->id;
1966                 $record->name = substr($key, 4);
1967                 $record->value = $value;
1968                 lti_update_config($record);
1969             }
1970             if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
1971                 $record = new \StdClass();
1972                 $record->typeid = $type->id;
1973                 $record->name = substr($key, 11);
1974                 $record->value = $value;
1975                 lti_update_config($record);
1976             }
1977         }
1978         require_once($CFG->libdir.'/modinfolib.php');
1979         if ($clearcache) {
1980             $sql = "SELECT DISTINCT course
1981                       FROM {lti}
1982                      WHERE typeid = ?";
1984             $courses = $DB->get_fieldset_sql($sql, array($type->id));
1986             foreach ($courses as $courseid) {
1987                 rebuild_course_cache($courseid, true);
1988             }
1989         }
1990     }
1993 function lti_add_type($type, $config) {
1994     global $USER, $SITE, $DB;
1996     lti_prepare_type_for_save($type, $config);
1998     if (!isset($type->state)) {
1999         $type->state = LTI_TOOL_STATE_PENDING;
2000     }
2002     if (!isset($type->timecreated)) {
2003         $type->timecreated = time();
2004     }
2006     if (!isset($type->createdby)) {
2007         $type->createdby = $USER->id;
2008     }
2010     if (!isset($type->course)) {
2011         $type->course = $SITE->id;
2012     }
2014     // Create a salt value to be used for signing passed data to extension services
2015     // The outcome service uses the service salt on the instance. This can be used
2016     // for communication with services not related to a specific LTI instance.
2017     $config->lti_servicesalt = uniqid('', true);
2019     $id = $DB->insert_record('lti_types', $type);
2021     if ($id) {
2022         foreach ($config as $key => $value) {
2023             if (!is_null($value)) {
2024                 $fieldparts = preg_split("/(lti|ltiservice)_/i", $key);
2025                 // If array has only one element, it did not start with the pattern.
2026                 if (count($fieldparts) < 2) {
2027                     continue;
2028                 }
2029                 $fieldname = $fieldparts[1];
2031                 $record = new \StdClass();
2032                 $record->typeid = $id;
2033                 $record->name = $fieldname;
2034                 $record->value = $value;
2036                 lti_add_config($record);
2037             }
2038         }
2039     }
2041     return $id;
2044 /**
2045  * Given an array of tool proxies, filter them based on their state
2046  *
2047  * @param array $toolproxies An array of lti_tool_proxies records
2048  * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants
2049  *
2050  * @return array
2051  */
2052 function lti_filter_tool_proxy_types(array $toolproxies, $state) {
2053     $return = array();
2054     foreach ($toolproxies as $key => $toolproxy) {
2055         if ($toolproxy->state == $state) {
2056             $return[$key] = $toolproxy;
2057         }
2058     }
2059     return $return;
2062 /**
2063  * Get the tool proxy instance given its GUID
2064  *
2065  * @param string  $toolproxyguid   Tool proxy GUID value
2066  *
2067  * @return object
2068  */
2069 function lti_get_tool_proxy_from_guid($toolproxyguid) {
2070     global $DB;
2072     $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid));
2074     return $toolproxy;
2077 /**
2078  * Get the tool proxy instance given its registration URL
2079  *
2080  * @param string $regurl Tool proxy registration URL
2081  *
2082  * @return array The record of the tool proxy with this url
2083  */
2084 function lti_get_tool_proxies_from_registration_url($regurl) {
2085     global $DB;
2087     return $DB->get_records_sql(
2088         'SELECT * FROM {lti_tool_proxies}
2089         WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl',
2090         array('regurl' => $regurl)
2091     );
2094 /**
2095  * Generates some of the tool proxy configuration based on the admin configuration details
2096  *
2097  * @param int $id
2098  *
2099  * @return mixed Tool Proxy details
2100  */
2101 function lti_get_tool_proxy($id) {
2102     global $DB;
2104     $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id));
2105     return $toolproxy;
2108 /**
2109  * Returns lti tool proxies.
2110  *
2111  * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them
2112  * @return array of basicLTI types
2113  */
2114 function lti_get_tool_proxies($orphanedonly) {
2115     global $DB;
2117     if ($orphanedonly) {
2118         $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));
2119         $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
2120         foreach ($proxies as $key => $value) {
2121             if (in_array($value->id, $usedproxyids)) {
2122                 unset($proxies[$key]);
2123             }
2124         }
2125         return $proxies;
2126     } else {
2127         return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
2128     }
2131 /**
2132  * Generates some of the tool proxy configuration based on the admin configuration details
2133  *
2134  * @param int $id
2135  *
2136  * @return mixed  Tool Proxy details
2137  */
2138 function lti_get_tool_proxy_config($id) {
2139     $toolproxy = lti_get_tool_proxy($id);
2141     $tp = new \stdClass();
2142     $tp->lti_registrationname = $toolproxy->name;
2143     $tp->toolproxyid = $toolproxy->id;
2144     $tp->state = $toolproxy->state;
2145     $tp->lti_registrationurl = $toolproxy->regurl;
2146     $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered);
2147     $tp->lti_services = explode("\n", $toolproxy->serviceoffered);
2149     return $tp;
2152 /**
2153  * Update the database with a tool proxy instance
2154  *
2155  * @param object   $config    Tool proxy definition
2156  *
2157  * @return int  Record id number
2158  */
2159 function lti_add_tool_proxy($config) {
2160     global $USER, $DB;
2162     $toolproxy = new \stdClass();
2163     if (isset($config->lti_registrationname)) {
2164         $toolproxy->name = trim($config->lti_registrationname);
2165     }
2166     if (isset($config->lti_registrationurl)) {
2167         $toolproxy->regurl = trim($config->lti_registrationurl);
2168     }
2169     if (isset($config->lti_capabilities)) {
2170         $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities);
2171     } else {
2172         $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities()));
2173     }
2174     if (isset($config->lti_services)) {
2175         $toolproxy->serviceoffered = implode("\n", $config->lti_services);
2176     } else {
2177         $func = function($s) {
2178             return $s->get_id();
2179         };
2180         $servicenames = array_map($func, lti_get_services());
2181         $toolproxy->serviceoffered = implode("\n", $servicenames);
2182     }
2183     if (isset($config->toolproxyid) && !empty($config->toolproxyid)) {
2184         $toolproxy->id = $config->toolproxyid;
2185         if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) {
2186             $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
2187             $toolproxy->guid = random_string();
2188             $toolproxy->secret = random_string();
2189         }
2190         $id = lti_update_tool_proxy($toolproxy);
2191     } else {
2192         $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;
2193         $toolproxy->timemodified = time();
2194         $toolproxy->timecreated = $toolproxy->timemodified;
2195         if (!isset($toolproxy->createdby)) {
2196             $toolproxy->createdby = $USER->id;
2197         }
2198         $toolproxy->guid = random_string();
2199         $toolproxy->secret = random_string();
2200         $id = $DB->insert_record('lti_tool_proxies', $toolproxy);
2201     }
2203     return $id;
2206 /**
2207  * Updates a tool proxy in the database
2208  *
2209  * @param object  $toolproxy   Tool proxy
2210  *
2211  * @return int    Record id number
2212  */
2213 function lti_update_tool_proxy($toolproxy) {
2214     global $DB;
2216     $toolproxy->timemodified = time();
2217     $id = $DB->update_record('lti_tool_proxies', $toolproxy);
2219     return $id;
2222 /**
2223  * Delete a Tool Proxy
2224  *
2225  * @param int $id   Tool Proxy id
2226  */
2227 function lti_delete_tool_proxy($id) {
2228     global $DB;
2229     $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id));
2230     $tools = $DB->get_records('lti_types', array('toolproxyid' => $id));
2231     foreach ($tools as $tool) {
2232         lti_delete_type($tool->id);
2233     }
2234     $DB->delete_records('lti_tool_proxies', array('id' => $id));
2237 /**
2238  * Add a tool configuration in the database
2239  *
2240  * @param object $config   Tool configuration
2241  *
2242  * @return int Record id number
2243  */
2244 function lti_add_config($config) {
2245     global $DB;
2247     return $DB->insert_record('lti_types_config', $config);
2250 /**
2251  * Updates a tool configuration in the database
2252  *
2253  * @param object  $config   Tool configuration
2254  *
2255  * @return mixed Record id number
2256  */
2257 function lti_update_config($config) {
2258     global $DB;
2260     $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));
2262     if ($old) {
2263         $config->id = $old->id;
2264         $return = $DB->update_record('lti_types_config', $config);
2265     } else {
2266         $return = $DB->insert_record('lti_types_config', $config);
2267     }
2268     return $return;
2271 /**
2272  * Gets the tool settings
2273  *
2274  * @param int  $toolproxyid   Id of tool proxy record
2275  * @param int  $courseid      Id of course (null if system settings)
2276  * @param int  $instanceid    Id of course module (null if system or context settings)
2277  *
2278  * @return array  Array settings
2279  */
2280 function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) {
2281     global $DB;
2283     $settings = array();
2284     $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
2285         'course' => $courseid, 'coursemoduleid' => $instanceid));
2286     if ($settingsstr !== false) {
2287         $settings = json_decode($settingsstr, true);
2288     }
2289     return $settings;
2292 /**
2293  * Sets the tool settings (
2294  *
2295  * @param array  $settings      Array of settings
2296  * @param int    $toolproxyid   Id of tool proxy record
2297  * @param int    $courseid      Id of course (null if system settings)
2298  * @param int    $instanceid    Id of course module (null if system or context settings)
2299  */
2300 function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) {
2301     global $DB;
2303     $json = json_encode($settings);
2304     $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
2305         'course' => $courseid, 'coursemoduleid' => $instanceid));
2306     if ($record !== false) {
2307         $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
2308     } else {
2309         $record = new \stdClass();
2310         $record->toolproxyid = $toolproxyid;
2311         $record->course = $courseid;
2312         $record->coursemoduleid = $instanceid;
2313         $record->settings = $json;
2314         $record->timecreated = time();
2315         $record->timemodified = $record->timecreated;
2316         $DB->insert_record('lti_tool_settings', $record);
2317     }
2320 /**
2321  * Signs the petition to launch the external tool using OAuth
2322  *
2323  * @param array  $oldparms     Parameters to be passed for signing
2324  * @param string $endpoint     url of the external tool
2325  * @param string $method       Method for sending the parameters (e.g. POST)
2326  * @param string $oauthconsumerkey
2327  * @param string $oauthconsumersecret
2328  * @return array|null
2329  */
2330 function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
2332     $parms = $oldparms;
2334     $testtoken = '';
2336     // TODO: Switch to core oauthlib once implemented - MDL-30149.
2337     $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1();
2338     $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null);
2339     $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms);
2340     $accreq->sign_request($hmacmethod, $testconsumer, $testtoken);
2342     $newparms = $accreq->get_parameters();
2344     return $newparms;
2347 /**
2348  * Posts the launch petition HTML
2349  *
2350  * @param array $newparms   Signed parameters
2351  * @param string $endpoint  URL of the external tool
2352  * @param bool $debug       Debug (true/false)
2353  * @return string
2354  */
2355 function lti_post_launch_html($newparms, $endpoint, $debug=false) {
2356     $r = "<form action=\"" . $endpoint .
2357         "\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n";
2359     // Contruct html for the launch parameters.
2360     foreach ($newparms as $key => $value) {
2361         $key = htmlspecialchars($key);
2362         $value = htmlspecialchars($value);
2363         if ( $key == "ext_submit" ) {
2364             $r .= "<input type=\"submit\"";
2365         } else {
2366             $r .= "<input type=\"hidden\" name=\"{$key}\"";
2367         }
2368         $r .= " value=\"";
2369         $r .= $value;
2370         $r .= "\"/>\n";
2371     }
2373     if ( $debug ) {
2374         $r .= "<script language=\"javascript\"> \n";
2375         $r .= "  //<![CDATA[ \n";
2376         $r .= "function basicltiDebugToggle() {\n";
2377         $r .= "    var ele = document.getElementById(\"basicltiDebug\");\n";
2378         $r .= "    if (ele.style.display == \"block\") {\n";
2379         $r .= "        ele.style.display = \"none\";\n";
2380         $r .= "    }\n";
2381         $r .= "    else {\n";
2382         $r .= "        ele.style.display = \"block\";\n";
2383         $r .= "    }\n";
2384         $r .= "} \n";
2385         $r .= "  //]]> \n";
2386         $r .= "</script>\n";
2387         $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";
2388         $r .= get_string("toggle_debug_data", "lti")."</a>\n";
2389         $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
2390         $r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n";
2391         $r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
2392         $r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n";
2393         foreach ($newparms as $key => $value) {
2394             $key = htmlspecialchars($key);
2395             $value = htmlspecialchars($value);
2396             $r .= "$key = $value<br/>\n";
2397         }
2398         $r .= "&nbsp;<br/>\n";
2399         $r .= "</div>\n";
2400     }
2401     $r .= "</form>\n";
2403     if ( ! $debug ) {
2404         $r .= " <script type=\"text/javascript\"> \n" .
2405             "  //<![CDATA[ \n" .
2406             "    document.ltiLaunchForm.submit(); \n" .
2407             "  //]]> \n" .
2408             " </script> \n";
2409     }
2410     return $r;
2413 function lti_get_type($typeid) {
2414     global $DB;
2416     return $DB->get_record('lti_types', array('id' => $typeid));
2419 function lti_get_launch_container($lti, $toolconfig) {
2420     if (empty($lti->launchcontainer)) {
2421         $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
2422     }
2424     if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
2425         if (isset($toolconfig['launchcontainer'])) {
2426             $launchcontainer = $toolconfig['launchcontainer'];
2427         }
2428     } else {
2429         $launchcontainer = $lti->launchcontainer;
2430     }
2432     if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {
2433         $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
2434     }
2436     $devicetype = core_useragent::get_device_type();
2438     // Scrolling within the object element doesn't work on iOS or Android
2439     // Opening the popup window also had some issues in testing
2440     // For mobile devices, always take up the entire screen to ensure the best experience.
2441     if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) {
2442         $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW;
2443     }
2445     return $launchcontainer;
2448 function lti_request_is_using_ssl() {
2449     global $CFG;
2450     return (stripos($CFG->wwwroot, 'https://') === 0);
2453 function lti_ensure_url_is_https($url) {
2454     if (!strstr($url, '://')) {
2455         $url = 'https://' . $url;
2456     } else {
2457         // If the URL starts with http, replace with https.
2458         if (stripos($url, 'http://') === 0) {
2459             $url = 'https://' . substr($url, 7);
2460         }
2461     }
2463     return $url;
2466 /**
2467  * Determines if we should try to log the request
2468  *
2469  * @param string $rawbody
2470  * @return bool
2471  */
2472 function lti_should_log_request($rawbody) {
2473     global $CFG;
2475     if (empty($CFG->mod_lti_log_users)) {
2476         return false;
2477     }
2479     $logusers = explode(',', $CFG->mod_lti_log_users);
2480     if (empty($logusers)) {
2481         return false;
2482     }
2484     try {
2485         $xml = new \SimpleXMLElement($rawbody);
2486         $ns  = $xml->getNamespaces();
2487         $ns  = array_shift($ns);
2488         $xml->registerXPathNamespace('lti', $ns);
2489         $requestuserid = '';
2490         if ($node = $xml->xpath('//lti:userId')) {
2491             $node = $node[0];
2492             $requestuserid = clean_param((string) $node, PARAM_INT);
2493         } else if ($node = $xml->xpath('//lti:sourcedId')) {
2494             $node = $node[0];
2495             $resultjson = json_decode((string) $node);
2496             $requestuserid = clean_param($resultjson->data->userid, PARAM_INT);
2497         }
2498     } catch (Exception $e) {
2499         return false;
2500     }
2502     if (empty($requestuserid) or !in_array($requestuserid, $logusers)) {
2503         return false;
2504     }
2506     return true;
2509 /**
2510  * Logs the request to a file in temp dir.
2511  *
2512  * @param string $rawbody
2513  */
2514 function lti_log_request($rawbody) {
2515     if ($tempdir = make_temp_directory('mod_lti', false)) {
2516         if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
2517             $content  = "Request Headers:\n";
2518             foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
2519                 $content .= "$header: $value\n";
2520             }
2521             $content .= "Request Body:\n";
2522             $content .= $rawbody;
2524             file_put_contents($tempfile, $content);
2525             chmod($tempfile, 0644);
2526         }
2527     }
2530 /**
2531  * Log an LTI response.
2532  *
2533  * @param string $responsexml The response XML
2534  * @param Exception $e If there was an exception, pass that too
2535  */
2536 function lti_log_response($responsexml, $e = null) {
2537     if ($tempdir = make_temp_directory('mod_lti', false)) {
2538         if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
2539             $content = '';
2540             if ($e instanceof Exception) {
2541                 $info = get_exception_info($e);
2543                 $content .= "Exception:\n";
2544                 $content .= "Message: $info->message\n";
2545                 $content .= "Debug info: $info->debuginfo\n";
2546                 $content .= "Backtrace:\n";
2547                 $content .= format_backtrace($info->backtrace, true);
2548                 $content .= "\n";
2549             }
2550             $content .= "Response XML:\n";
2551             $content .= $responsexml;
2553             file_put_contents($tempfile, $content);
2554             chmod($tempfile, 0644);
2555         }
2556     }
2559 /**
2560  * Fetches LTI type configuration for an LTI instance
2561  *
2562  * @param stdClass $instance
2563  * @return array Can be empty if no type is found
2564  */
2565 function lti_get_type_config_by_instance($instance) {
2566     $typeid = null;
2567     if (empty($instance->typeid)) {
2568         $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
2569         if ($tool) {
2570             $typeid = $tool->id;
2571         }
2572     } else {
2573         $typeid = $instance->typeid;
2574     }
2575     if (!empty($typeid)) {
2576         return lti_get_type_config($typeid);
2577     }
2578     return array();
2581 /**
2582  * Enforce type config settings onto the LTI instance
2583  *
2584  * @param stdClass $instance
2585  * @param array $typeconfig
2586  */
2587 function lti_force_type_config_settings($instance, array $typeconfig) {
2588     $forced = array(
2589         'instructorchoicesendname'      => 'sendname',
2590         'instructorchoicesendemailaddr' => 'sendemailaddr',
2591         'instructorchoiceacceptgrades'  => 'acceptgrades',
2592     );
2594     foreach ($forced as $instanceparam => $typeconfigparam) {
2595         if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) {
2596             $instance->$instanceparam = $typeconfig[$typeconfigparam];
2597         }
2598     }
2601 /**
2602  * Initializes an array with the capabilities supported by the LTI module
2603  *
2604  * @return array List of capability names (without a dollar sign prefix)
2605  */
2606 function lti_get_capabilities() {
2608     $capabilities = array(
2609        'basic-lti-launch-request' => '',
2610        'ContentItemSelectionRequest' => '',
2611        'ToolProxyRegistrationRequest' => '',
2612        'Context.id' => 'context_id',
2613        'Context.title' => 'context_title',
2614        'Context.label' => 'context_label',
2615        'Context.sourcedId' => 'lis_course_section_sourcedid',
2616        'Context.longDescription' => '$COURSE->summary',
2617        'Context.timeFrame.begin' => '$COURSE->startdate',
2618        'CourseSection.title' => 'context_title',
2619        'CourseSection.label' => 'context_label',
2620        'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
2621        'CourseSection.longDescription' => '$COURSE->summary',
2622        'CourseSection.timeFrame.begin' => '$COURSE->startdate',
2623        'ResourceLink.id' => 'resource_link_id',
2624        'ResourceLink.title' => 'resource_link_title',
2625        'ResourceLink.description' => 'resource_link_description',
2626        'User.id' => 'user_id',
2627        'User.username' => '$USER->username',
2628        'Person.name.full' => 'lis_person_name_full',
2629        'Person.name.given' => 'lis_person_name_given',
2630        'Person.name.family' => 'lis_person_name_family',
2631        'Person.email.primary' => 'lis_person_contact_email_primary',
2632        'Person.sourcedId' => 'lis_person_sourcedid',
2633        'Person.name.middle' => '$USER->middlename',
2634        'Person.address.street1' => '$USER->address',
2635        'Person.address.locality' => '$USER->city',
2636        'Person.address.country' => '$USER->country',
2637        'Person.address.timezone' => '$USER->timezone',
2638        'Person.phone.primary' => '$USER->phone1',
2639        'Person.phone.mobile' => '$USER->phone2',
2640        'Person.webaddress' => '$USER->url',
2641        'Membership.role' => 'roles',
2642        'Result.sourcedId' => 'lis_result_sourcedid',
2643        'Result.autocreate' => 'lis_outcome_service_url',
2644        'Moodle.Person.userGroupIds' => null);
2646     return $capabilities;
2650 /**
2651  * Initializes an array with the services supported by the LTI module
2652  *
2653  * @return array List of services
2654  */
2655 function lti_get_services() {
2657     $services = array();
2658     $definedservices = core_component::get_plugin_list('ltiservice');
2659     foreach ($definedservices as $name => $location) {
2660         $classname = "\\ltiservice_{$name}\\local\\service\\{$name}";
2661         $services[] = new $classname();
2662     }
2664     return $services;
2668 /**
2669  * Initializes an instance of the named service
2670  *
2671  * @param string $servicename Name of service
2672  *
2673  * @return bool|\mod_lti\local\ltiservice\service_base Service
2674  */
2675 function lti_get_service_by_name($servicename) {
2677     $service = false;
2678     $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}";
2679     if (class_exists($classname)) {
2680         $service = new $classname();
2681     }
2683     return $service;
2687 /**
2688  * Finds a service by id
2689  *
2690  * @param \mod_lti\local\ltiservice\service_base[] $services Array of services
2691  * @param string $resourceid  ID of resource
2692  *
2693  * @return mod_lti\local\ltiservice\service_base Service
2694  */
2695 function lti_get_service_by_resource_id($services, $resourceid) {
2697     $service = false;
2698     foreach ($services as $aservice) {
2699         foreach ($aservice->get_resources() as $resource) {
2700             if ($resource->get_id() === $resourceid) {
2701                 $service = $aservice;
2702                 break 2;
2703             }
2704         }
2705     }
2707     return $service;
2711 /**
2712  * Extracts the named contexts from a tool proxy
2713  *
2714  * @param object $json
2715  *
2716  * @return array Contexts
2717  */
2718 function lti_get_contexts($json) {
2720     $contexts = array();
2721     if (isset($json->{'@context'})) {
2722         foreach ($json->{'@context'} as $context) {
2723             if (is_object($context)) {
2724                 $contexts = array_merge(get_object_vars($context), $contexts);
2725             }
2726         }
2727     }
2729     return $contexts;
2733 /**
2734  * Converts an ID to a fully-qualified ID
2735  *
2736  * @param array $contexts
2737  * @param string $id
2738  *
2739  * @return string Fully-qualified ID
2740  */
2741 function lti_get_fqid($contexts, $id) {
2743     $parts = explode(':', $id, 2);
2744     if (count($parts) > 1) {
2745         if (array_key_exists($parts[0], $contexts)) {
2746             $id = $contexts[$parts[0]] . $parts[1];
2747         }
2748     }
2750     return $id;
2754 /**
2755  * Returns the icon for the given tool type
2756  *
2757  * @param stdClass $type The tool type
2758  *
2759  * @return string The url to the tool type's corresponding icon
2760  */
2761 function get_tool_type_icon_url(stdClass $type) {
2762     global $OUTPUT;
2764     $iconurl = $type->secureicon;
2766     if (empty($iconurl)) {
2767         $iconurl = $type->icon;
2768     }
2770     if (empty($iconurl)) {
2771         $iconurl = $OUTPUT->image_url('icon', 'lti')->out();
2772     }
2774     return $iconurl;
2777 /**
2778  * Returns the edit url for the given tool type
2779  *
2780  * @param stdClass $type The tool type
2781  *
2782  * @return string The url to edit the tool type
2783  */
2784 function get_tool_type_edit_url(stdClass $type) {
2785     $url = new moodle_url('/mod/lti/typessettings.php',
2786                           array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
2787     return $url->out();
2790 /**
2791  * Returns the edit url for the given tool proxy.
2792  *
2793  * @param stdClass $proxy The tool proxy
2794  *
2795  * @return string The url to edit the tool type
2796  */
2797 function get_tool_proxy_edit_url(stdClass $proxy) {
2798     $url = new moodle_url('/mod/lti/registersettings.php',
2799                           array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));
2800     return $url->out();
2803 /**
2804  * Returns the course url for the given tool type
2805  *
2806  * @param stdClass $type The tool type
2807  *
2808  * @return string The url to the course of the tool type, void if it is a site wide type
2809  */
2810 function get_tool_type_course_url(stdClass $type) {
2811     if ($type->course != 1) {
2812         $url = new moodle_url('/course/view.php', array('id' => $type->course));
2813         return $url->out();
2814     }
2815     return null;
2818 /**
2819  * Returns the icon and edit urls for the tool type and the course url if it is a course type.
2820  *
2821  * @param stdClass $type The tool type
2822  *
2823  * @return array The urls of the tool type
2824  */
2825 function get_tool_type_urls(stdClass $type) {
2826     $courseurl = get_tool_type_course_url($type);
2828     $urls = array(
2829         'icon' => get_tool_type_icon_url($type),
2830         'edit' => get_tool_type_edit_url($type),
2831     );
2833     if ($courseurl) {
2834         $urls['course'] = $courseurl;
2835     }
2837     return $urls;
2840 /**
2841  * Returns the icon and edit urls for the tool proxy.
2842  *
2843  * @param stdClass $proxy The tool proxy
2844  *
2845  * @return array The urls of the tool proxy
2846  */
2847 function get_tool_proxy_urls(stdClass $proxy) {
2848     global $OUTPUT;
2850     $urls = array(
2851         'icon' => $OUTPUT->image_url('icon', 'lti')->out(),
2852         'edit' => get_tool_proxy_edit_url($proxy),
2853     );
2855     return $urls;
2858 /**
2859  * Returns information on the current state of the tool type
2860  *
2861  * @param stdClass $type The tool type
2862  *
2863  * @return array An array with a text description of the state, and boolean for whether it is in each state:
2864  * pending, configured, rejected, unknown
2865  */
2866 function get_tool_type_state_info(stdClass $type) {
2867     $isconfigured = false;
2868     $ispending = false;
2869     $isrejected = false;
2870     $isunknown = false;
2871     switch ($type->state) {
2872         case LTI_TOOL_STATE_CONFIGURED:
2873             $state = get_string('active', 'mod_lti');
2874             $isconfigured = true;
2875             break;
2876         case LTI_TOOL_STATE_PENDING:
2877             $state = get_string('pending', 'mod_lti');
2878             $ispending = true;
2879             break;
2880         case LTI_TOOL_STATE_REJECTED:
2881             $state = get_string('rejected', 'mod_lti');
2882             $isrejected = true;
2883             break;
2884         default:
2885             $state = get_string('unknownstate', 'mod_lti');
2886             $isunknown = true;
2887             break;
2888     }
2890     return array(
2891         'text' => $state,
2892         'pending' => $ispending,
2893         'configured' => $isconfigured,
2894         'rejected' => $isrejected,
2895         'unknown' => $isunknown
2896     );
2899 /**
2900  * Returns a summary of each LTI capability this tool type requires in plain language
2901  *
2902  * @param stdClass $type The tool type
2903  *
2904  * @return array An array of text descriptions of each of the capabilities this tool type requires
2905  */
2906 function get_tool_type_capability_groups($type) {
2907     $capabilities = lti_get_enabled_capabilities($type);
2908     $groups = array();
2909     $hascourse = false;
2910     $hasactivities = false;
2911     $hasuseraccount = false;
2912     $hasuserpersonal = false;
2914     foreach ($capabilities as $capability) {
2915         // Bail out early if we've already found all groups.
2916         if (count($groups) >= 4) {
2917             continue;
2918         }
2920         if (!$hascourse && preg_match('/^CourseSection/', $capability)) {
2921             $hascourse = true;
2922             $groups[] = get_string('courseinformation', 'mod_lti');
2923         } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) {
2924             $hasactivities = true;
2925             $groups[] = get_string('courseactivitiesorresources', 'mod_lti');
2926         } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) {
2927             $hasuseraccount = true;
2928             $groups[] = get_string('useraccountinformation', 'mod_lti');
2929         } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) {
2930             $hasuserpersonal = true;
2931             $groups[] = get_string('userpersonalinformation', 'mod_lti');
2932         }
2933     }
2935     return $groups;
2939 /**
2940  * Returns the ids of each instance of this tool type
2941  *
2942  * @param stdClass $type The tool type
2943  *
2944  * @return array An array of ids of the instances of this tool type
2945  */
2946 function get_tool_type_instance_ids($type) {
2947     global $DB;
2949     return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id)));
2952 /**
2953  * Serialises this tool type
2954  *
2955  * @param stdClass $type The tool type
2956  *
2957  * @return array An array of values representing this type
2958  */
2959 function serialise_tool_type(stdClass $type) {
2960     $capabilitygroups = get_tool_type_capability_groups($type);
2961     $instanceids = get_tool_type_instance_ids($type);
2962     // Clean the name. We don't want tags here.
2963     $name = clean_param($type->name, PARAM_NOTAGS);
2964     if (!empty($type->description)) {
2965         // Clean the description. We don't want tags here.
2966         $description = clean_param($type->description, PARAM_NOTAGS);
2967     } else {
2968         $description = get_string('editdescription', 'mod_lti');
2969     }
2970     return array(
2971         'id' => $type->id,
2972         'name' => $name,
2973         'description' => $description,
2974         'urls' => get_tool_type_urls($type),
2975         'state' => get_tool_type_state_info($type),
2976         'hascapabilitygroups' => !empty($capabilitygroups),
2977         'capabilitygroups' => $capabilitygroups,
2978         // Course ID of 1 means it's not linked to a course.
2979         'courseid' => $type->course == 1 ? 0 : $type->course,
2980         'instanceids' => $instanceids,
2981         'instancecount' => count($instanceids)
2982     );
2985 /**
2986  * Serialises this tool proxy.
2987  *
2988  * @param stdClass $proxy The tool proxy
2989  *
2990  * @return array An array of values representing this type
2991  */
2992 function serialise_tool_proxy(stdClass $proxy) {
2993     return array(
2994         'id' => $proxy->id,
2995         'name' => $proxy->name,
2996         'description' => get_string('activatetoadddescription', 'mod_lti'),
2997         'urls' => get_tool_proxy_urls($proxy),
2998         'state' => array(
2999             'text' => get_string('pending', 'mod_lti'),
3000             'pending' => true,
3001             'configured' => false,
3002             'rejected' => false,
3003             'unknown' => false
3004         ),
3005         'hascapabilitygroups' => true,
3006         'capabilitygroups' => array(),
3007         'courseid' => 0,
3008         'instanceids' => array(),
3009         'instancecount' => 0
3010     );
3013 /**
3014  * Loads the cartridge information into the tool type, if the launch url is for a cartridge file
3015  *
3016  * @param stdClass $type The tool type object to be filled in
3017  * @since Moodle 3.1
3018  */
3019 function lti_load_type_if_cartridge($type) {
3020     if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) {
3021         lti_load_type_from_cartridge($type->lti_toolurl, $type);
3022     }
3025 /**
3026  * Loads the cartridge information into the new tool, if the launch url is for a cartridge file
3027  *
3028  * @param stdClass $lti The tools config
3029  * @since Moodle 3.1
3030  */
3031 function lti_load_tool_if_cartridge($lti) {
3032     if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) {
3033         lti_load_tool_from_cartridge($lti->toolurl, $lti);
3034     }
3037 /**
3038  * Determines if the given url is for a IMS basic cartridge
3039  *
3040  * @param  string $url The url to be checked
3041  * @return True if the url is for a cartridge
3042  * @since Moodle 3.1
3043  */
3044 function lti_is_cartridge($url) {
3045     // If it is empty, it's not a cartridge.
3046     if (empty($url)) {
3047         return false;
3048     }
3049     // If it has xml at the end of the url, it's a cartridge.
3050     if (preg_match('/\.xml$/', $url)) {
3051         return true;
3052     }
3053     // Even if it doesn't have .xml, load the url to check if it's a cartridge..
3054     try {
3055         $toolinfo = lti_load_cartridge($url,
3056             array(
3057                 "launch_url" => "launchurl"
3058             )
3059         );
3060         if (!empty($toolinfo['launchurl'])) {
3061             return true;
3062         }
3063     } catch (moodle_exception $e) {
3064         return false; // Error loading the xml, so it's not a cartridge.
3065     }
3066     return false;
3069 /**
3070  * Allows you to load settings for an external tool type from an IMS cartridge.
3071  *
3072  * @param  string   $url     The URL to the cartridge
3073  * @param  stdClass $type    The tool type object to be filled in
3074  * @throws moodle_exception if the cartridge could not be loaded correctly
3075  * @since Moodle 3.1
3076  */
3077 function lti_load_type_from_cartridge($url, $type) {
3078     $toolinfo = lti_load_cartridge($url,
3079         array(
3080             "title" => "lti_typename",
3081             "launch_url" => "lti_toolurl",
3082             "description" => "lti_description",
3083             "icon" => "lti_icon",
3084             "secure_icon" => "lti_secureicon"
3085         ),
3086         array(
3087             "icon_url" => "lti_extension_icon",
3088             "secure_icon_url" => "lti_extension_secureicon"
3089         )
3090     );
3091     // If an activity name exists, unset the cartridge name so we don't override it.
3092     if (isset($type->lti_typename)) {
3093         unset($toolinfo['lti_typename']);
3094     }
3096     // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
3097     if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
3098         $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
3099     }
3100     unset($toolinfo['lti_extension_icon']);
3102     if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
3103         $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
3104     }
3105     unset($toolinfo['lti_extension_secureicon']);
3107     // Ensure Custom icons aren't overridden by cartridge params.
3108     if (!empty($type->lti_icon)) {
3109         unset($toolinfo['lti_icon']);
3110     }
3112     if (!empty($type->lti_secureicon)) {
3113         unset($toolinfo['lti_secureicon']);
3114     }
3116     foreach ($toolinfo as $property => $value) {
3117         $type->$property = $value;
3118     }
3121 /**
3122  * Allows you to load in the configuration for an external tool from an IMS cartridge.
3123  *
3124  * @param  string   $url    The URL to the cartridge
3125  * @param  stdClass $lti    LTI object
3126  * @throws moodle_exception if the cartridge could not be loaded correctly
3127  * @since Moodle 3.1
3128  */
3129 function lti_load_tool_from_cartridge($url, $lti) {
3130     $toolinfo = lti_load_cartridge($url,
3131         array(
3132             "title" => "name",
3133             "launch_url" => "toolurl",
3134             "secure_launch_url" => "securetoolurl",
3135             "description" => "intro",
3136             "icon" => "icon",
3137             "secure_icon" => "secureicon"
3138         ),
3139         array(
3140             "icon_url" => "extension_icon",
3141             "secure_icon_url" => "extension_secureicon"
3142         )
3143     );
3144     // If an activity name exists, unset the cartridge name so we don't override it.
3145     if (isset($lti->name)) {
3146         unset($toolinfo['name']);
3147     }
3149     // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
3150     if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
3151         $toolinfo['icon'] = $toolinfo['extension_icon'];
3152     }
3153     unset($toolinfo['extension_icon']);
3155     if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
3156         $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
3157     }
3158     unset($toolinfo['extension_secureicon']);
3160     foreach ($toolinfo as $property => $value) {
3161         $lti->$property = $value;
3162     }
3165 /**
3166  * Search for a tag within an XML DOMDocument
3167  *
3168  * @param  string $url The url of the cartridge to be loaded
3169  * @param  array  $map The map of tags to keys in the return array
3170  * @param  array  $propertiesmap The map of properties to keys in the return array
3171  * @return array An associative array with the given keys and their values from the cartridge
3172  * @throws moodle_exception if the cartridge could not be loaded correctly
3173  * @since Moodle 3.1
3174  */
3175 function lti_load_cartridge($url, $map, $propertiesmap = array()) {
3176     global $CFG;
3177     require_once($CFG->libdir. "/filelib.php");
3179     $curl = new curl();
3180     $response = $curl->get($url);
3182     // TODO MDL-46023 Replace this code with a call to the new library.
3183     $origerrors = libxml_use_internal_errors(true);
3184     $origentity = libxml_disable_entity_loader(true);
3185     libxml_clear_errors();
3187     $document = new DOMDocument();
3188     @$document->loadXML($response, LIBXML_DTDLOAD | LIBXML_DTDATTR);
3190     $cartridge = new DomXpath($document);
3192     $errors = libxml_get_errors();
3194     libxml_clear_errors();
3195     libxml_use_internal_errors($origerrors);
3196     libxml_disable_entity_loader($origentity);
3198     if (count($errors) > 0) {
3199         $message = 'Failed to load cartridge.';
3200         foreach ($errors as $error) {
3201             $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line;
3202         }
3203         throw new moodle_exception('errorreadingfile', '', '', $url, $message);
3204     }
3206     $toolinfo = array();
3207     foreach ($map as $tag => $key) {
3208         $value = get_tag($tag, $cartridge);
3209         if ($value) {
3210             $toolinfo[$key] = $value;
3211         }
3212     }
3213     if (!empty($propertiesmap)) {
3214         foreach ($propertiesmap as $property => $key) {
3215             $value = get_tag("property", $cartridge, $property);
3216             if ($value) {
3217                 $toolinfo[$key] = $value;
3218             }
3219         }
3220     }
3222     return $toolinfo;
3225 /**
3226  * Search for a tag within an XML DOMDocument
3227  *
3228  * @param  stdClass $tagname The name of the tag to search for
3229  * @param  XPath    $xpath   The XML to find the tag in
3230  * @param  XPath    $attribute The attribute to search for (if we should search for a child node with the given
3231  * value for the name attribute
3232  * @since Moodle 3.1
3233  */
3234 function get_tag($tagname, $xpath, $attribute = null) {
3235     if ($attribute) {
3236         $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]');
3237     } else {
3238         $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']');
3239     }
3240     if ($result->length > 0) {
3241         return $result->item(0)->nodeValue;
3242     }
3243     return null;