Rev 1 | Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>.//// This file is part of BasicLTI4Moodle//// BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)// consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web// based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI// specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS// are already supporting or going to support BasicLTI. This project Implements the consumer// for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.// BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem// at the GESSI research group at UPC.// SimpleLTI consumer for Moodle is an implementation of the early specification of LTI// by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a// Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.//// BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis// of the Universitat Politecnica de Catalunya http://www.upc.edu// Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu./*** This file contains the library of functions and constants for the lti module** @package mod_lti* @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis* marc.alier@upc.edu* @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu* @author Marc Alier* @author Jordi Piguillem* @author Nikolas Galanis* @author Chris Scribner* @copyright 2015 Vital Source Technologies http://vitalsource.com* @author Stephen Vickers* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die;// TODO: Switch to core oauthlib once implemented - MDL-30149.use mod_lti\helper;use moodle\mod\lti as lti;use Firebase\JWT\JWT;use Firebase\JWT\JWK;use Firebase\JWT\Key;use mod_lti\local\ltiopenid\jwks_helper;use mod_lti\local\ltiopenid\registration_helper;global $CFG;require_once($CFG->dirroot.'/mod/lti/OAuth.php');require_once($CFG->libdir.'/weblib.php');require_once($CFG->dirroot . '/course/modlib.php');require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i');define('LTI_LAUNCH_CONTAINER_DEFAULT', 1);define('LTI_LAUNCH_CONTAINER_EMBED', 2);define('LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS', 3);define('LTI_LAUNCH_CONTAINER_WINDOW', 4);define('LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW', 5);define('LTI_TOOL_STATE_ANY', 0);define('LTI_TOOL_STATE_CONFIGURED', 1);define('LTI_TOOL_STATE_PENDING', 2);define('LTI_TOOL_STATE_REJECTED', 3);define('LTI_TOOL_PROXY_TAB', 4);define('LTI_TOOL_PROXY_STATE_CONFIGURED', 1);define('LTI_TOOL_PROXY_STATE_PENDING', 2);define('LTI_TOOL_PROXY_STATE_ACCEPTED', 3);define('LTI_TOOL_PROXY_STATE_REJECTED', 4);define('LTI_SETTING_NEVER', 0);define('LTI_SETTING_ALWAYS', 1);define('LTI_SETTING_DELEGATE', 2);define('LTI_COURSEVISIBLE_NO', 0);define('LTI_COURSEVISIBLE_PRECONFIGURED', 1);define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2);define('LTI_VERSION_1', 'LTI-1p0');define('LTI_VERSION_2', 'LTI-2p0');define('LTI_VERSION_1P3', '1.3.0');define('LTI_RSA_KEY', 'RSA_KEY');define('LTI_JWK_KEYSET', 'JWK_KEYSET');define('LTI_DEFAULT_ORGID_SITEID', 'SITEID');define('LTI_DEFAULT_ORGID_SITEHOST', 'SITEHOST');define('LTI_ACCESS_TOKEN_LIFE', 3600);// Standard prefix for JWT claims.define('LTI_JWT_CLAIM_PREFIX', 'https://purl.imsglobal.org/spec/lti');/*** Return the mapping for standard message types to JWT message_type claim.** @return array*/function lti_get_jwt_message_type_mapping() {return array('basic-lti-launch-request' => 'LtiResourceLinkRequest','ContentItemSelectionRequest' => 'LtiDeepLinkingRequest','LtiDeepLinkingResponse' => 'ContentItemSelection','LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest',);}/*** Return the mapping for standard message parameters to JWT claim.** @return array*/function lti_get_jwt_claim_mapping() {$mapping = [];$services = lti_get_services();foreach ($services as $service) {$mapping = array_merge($mapping, $service->get_jwt_claim_mappings());}$mapping = array_merge($mapping, ['accept_copy_advice' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'accept_copy_advice','isarray' => false,'type' => 'boolean'],'accept_media_types' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'accept_media_types','isarray' => true],'accept_multiple' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'accept_multiple','isarray' => false,'type' => 'boolean'],'accept_presentation_document_targets' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'accept_presentation_document_targets','isarray' => true],'accept_types' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'accept_types','isarray' => true],'accept_unsigned' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'accept_unsigned','isarray' => false,'type' => 'boolean'],'auto_create' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'auto_create','isarray' => false,'type' => 'boolean'],'can_confirm' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'can_confirm','isarray' => false,'type' => 'boolean'],'content_item_return_url' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'deep_link_return_url','isarray' => false],'content_items' => ['suffix' => 'dl','group' => '','claim' => 'content_items','isarray' => true],'data' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'data','isarray' => false],'text' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'text','isarray' => false],'title' => ['suffix' => 'dl','group' => 'deep_linking_settings','claim' => 'title','isarray' => false],'lti_msg' => ['suffix' => 'dl','group' => '','claim' => 'msg','isarray' => false],'lti_log' => ['suffix' => 'dl','group' => '','claim' => 'log','isarray' => false],'lti_errormsg' => ['suffix' => 'dl','group' => '','claim' => 'errormsg','isarray' => false],'lti_errorlog' => ['suffix' => 'dl','group' => '','claim' => 'errorlog','isarray' => false],'context_id' => ['suffix' => '','group' => 'context','claim' => 'id','isarray' => false],'context_label' => ['suffix' => '','group' => 'context','claim' => 'label','isarray' => false],'context_title' => ['suffix' => '','group' => 'context','claim' => 'title','isarray' => false],'context_type' => ['suffix' => '','group' => 'context','claim' => 'type','isarray' => true],'for_user_id' => ['suffix' => '','group' => 'for_user','claim' => 'user_id','isarray' => false],'lis_course_offering_sourcedid' => ['suffix' => '','group' => 'lis','claim' => 'course_offering_sourcedid','isarray' => false],'lis_course_section_sourcedid' => ['suffix' => '','group' => 'lis','claim' => 'course_section_sourcedid','isarray' => false],'launch_presentation_css_url' => ['suffix' => '','group' => 'launch_presentation','claim' => 'css_url','isarray' => false],'launch_presentation_document_target' => ['suffix' => '','group' => 'launch_presentation','claim' => 'document_target','isarray' => false],'launch_presentation_height' => ['suffix' => '','group' => 'launch_presentation','claim' => 'height','isarray' => false],'launch_presentation_locale' => ['suffix' => '','group' => 'launch_presentation','claim' => 'locale','isarray' => false],'launch_presentation_return_url' => ['suffix' => '','group' => 'launch_presentation','claim' => 'return_url','isarray' => false],'launch_presentation_width' => ['suffix' => '','group' => 'launch_presentation','claim' => 'width','isarray' => false],'lis_person_contact_email_primary' => ['suffix' => '','group' => null,'claim' => 'email','isarray' => false],'lis_person_name_family' => ['suffix' => '','group' => null,'claim' => 'family_name','isarray' => false],'lis_person_name_full' => ['suffix' => '','group' => null,'claim' => 'name','isarray' => false],'lis_person_name_given' => ['suffix' => '','group' => null,'claim' => 'given_name','isarray' => false],'lis_person_sourcedid' => ['suffix' => '','group' => 'lis','claim' => 'person_sourcedid','isarray' => false],'user_id' => ['suffix' => '','group' => null,'claim' => 'sub','isarray' => false],'user_image' => ['suffix' => '','group' => null,'claim' => 'picture','isarray' => false],'roles' => ['suffix' => '','group' => '','claim' => 'roles','isarray' => true],'role_scope_mentor' => ['suffix' => '','group' => '','claim' => 'role_scope_mentor','isarray' => false],'deployment_id' => ['suffix' => '','group' => '','claim' => 'deployment_id','isarray' => false],'lti_message_type' => ['suffix' => '','group' => '','claim' => 'message_type','isarray' => false],'lti_version' => ['suffix' => '','group' => '','claim' => 'version','isarray' => false],'resource_link_description' => ['suffix' => '','group' => 'resource_link','claim' => 'description','isarray' => false],'resource_link_id' => ['suffix' => '','group' => 'resource_link','claim' => 'id','isarray' => false],'resource_link_title' => ['suffix' => '','group' => 'resource_link','claim' => 'title','isarray' => false],'tool_consumer_info_product_family_code' => ['suffix' => '','group' => 'tool_platform','claim' => 'product_family_code','isarray' => false],'tool_consumer_info_version' => ['suffix' => '','group' => 'tool_platform','claim' => 'version','isarray' => false],'tool_consumer_instance_contact_email' => ['suffix' => '','group' => 'tool_platform','claim' => 'contact_email','isarray' => false],'tool_consumer_instance_description' => ['suffix' => '','group' => 'tool_platform','claim' => 'description','isarray' => false],'tool_consumer_instance_guid' => ['suffix' => '','group' => 'tool_platform','claim' => 'guid','isarray' => false],'tool_consumer_instance_name' => ['suffix' => '','group' => 'tool_platform','claim' => 'name','isarray' => false],'tool_consumer_instance_url' => ['suffix' => '','group' => 'tool_platform','claim' => 'url','isarray' => false]]);return $mapping;}/*** Return the type of the instance, using domain matching if no explicit type is set.** @param object $instance the external tool activity settings* @return object|null* @since Moodle 3.9*/function lti_get_instance_type(object $instance): ?object {if (empty($instance->typeid)) {if (!$tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course)) {$tool = lti_get_tool_by_url_match($instance->securetoolurl, $instance->course);}return $tool;}return lti_get_type($instance->typeid);}/*** Return the launch data required for opening the external tool.** @param stdClass $instance the external tool activity settings* @param string $nonce the nonce value to use (applies to LTI 1.3 only)* @return array the endpoint URL and parameters (including the signature)* @since Moodle 3.0*/function lti_get_launch_data($instance, $nonce = '', $messagetype = 'basic-lti-launch-request', $foruserid = 0) {global $PAGE, $USER;$messagetype = $messagetype ? $messagetype : 'basic-lti-launch-request';$tool = lti_get_instance_type($instance);if ($tool) {$typeid = $tool->id;$ltiversion = $tool->ltiversion;} else {$typeid = null;$ltiversion = LTI_VERSION_1;}if ($typeid) {$typeconfig = lti_get_type_config($typeid);} else {// There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults.$typeconfig = (array)$instance;$typeconfig['sendname'] = $instance->instructorchoicesendname;$typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr;$typeconfig['customparameters'] = $instance->instructorcustomparameters;$typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades;$typeconfig['allowroster'] = $instance->instructorchoiceallowroster;$typeconfig['forcessl'] = '0';}if (isset($tool->toolproxyid)) {$toolproxy = lti_get_tool_proxy($tool->toolproxyid);$key = $toolproxy->guid;$secret = $toolproxy->secret;} else {$toolproxy = null;if (!empty($instance->resourcekey)) {$key = $instance->resourcekey;} else if ($ltiversion === LTI_VERSION_1P3) {$key = $tool->clientid;} else if (!empty($typeconfig['resourcekey'])) {$key = $typeconfig['resourcekey'];} else {$key = '';}if (!empty($instance->password)) {$secret = $instance->password;} else if (!empty($typeconfig['password'])) {$secret = $typeconfig['password'];} else {$secret = '';}}$endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl'];$endpoint = trim($endpoint);// If the current request is using SSL and a secure tool URL is specified, use it.if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) {$endpoint = trim($instance->securetoolurl);}// If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL.if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {if (!empty($instance->securetoolurl)) {$endpoint = trim($instance->securetoolurl);}if ($endpoint !== '') {$endpoint = lti_ensure_url_is_https($endpoint);}} else if ($endpoint !== '' && !strstr($endpoint, '://')) {$endpoint = 'http://' . $endpoint;}$orgid = lti_get_organizationid($typeconfig);$course = $PAGE->course;$islti2 = isset($tool->toolproxyid);$allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2, $messagetype, $foruserid);if ($islti2) {$requestparams = lti_build_request_lti2($tool, $allparams);} else {$requestparams = $allparams;}$requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype));$customstr = '';if (isset($typeconfig['customparameters'])) {$customstr = $typeconfig['customparameters'];}$services = lti_get_services();foreach ($services as $service) {[$endpoint, $customstr] = $service->override_endpoint($messagetype,$endpoint, $customstr, $instance->course, $instance);}$requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr,$instance->instructorcustomparameters, $islti2));$launchcontainer = lti_get_launch_container($instance, $typeconfig);$returnurlparams = array('course' => $course->id,'launch_container' => $launchcontainer,'instanceid' => $instance->id,'sesskey' => sesskey());// Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns.$url = new \moodle_url('/mod/lti/return.php', $returnurlparams);$returnurl = $url->out(false);if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {$returnurl = lti_ensure_url_is_https($returnurl);}$target = '';switch($launchcontainer) {case LTI_LAUNCH_CONTAINER_EMBED:case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS:$target = 'iframe';break;case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW:$target = 'frame';break;case LTI_LAUNCH_CONTAINER_WINDOW:$target = 'window';break;}if (!empty($target)) {$requestparams['launch_presentation_document_target'] = $target;}$requestparams['launch_presentation_return_url'] = $returnurl;// Add the parameters configured by the LTI services.if ($typeid && !$islti2) {$services = lti_get_services();foreach ($services as $service) {$serviceparameters = $service->get_launch_parameters('basic-lti-launch-request',$course->id, $USER->id , $typeid, $instance->id);foreach ($serviceparameters as $paramkey => $paramvalue) {$requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,$islti2);}}}// Allow request params to be updated by sub-plugins.$plugins = core_component::get_plugin_list('ltisource');foreach (array_keys($plugins) as $plugin) {$pluginparams = component_callback('ltisource_'.$plugin, 'before_launch',array($instance, $endpoint, $requestparams), array());if (!empty($pluginparams) && is_array($pluginparams)) {$requestparams = array_merge($requestparams, $pluginparams);}}if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) {if ($ltiversion !== LTI_VERSION_1P3) {$parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret);} else {$parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce);}$endpointurl = new \moodle_url($endpoint);$endpointparams = $endpointurl->params();// Strip querystring params in endpoint url from $parms to avoid duplication.if (!empty($endpointparams) && !empty($parms)) {foreach (array_keys($endpointparams) as $paramname) {if (isset($parms[$paramname])) {unset($parms[$paramname]);}}}} else {// If no key and secret, do the launch unsigned.$returnurlparams['unsigned'] = '1';$parms = $requestparams;}return array($endpoint, $parms);}/*** Launch an external tool activity.** @param stdClass $instance the external tool activity settings* @param int $foruserid for user param, optional* @return string The HTML code containing the javascript code for the launch*/function lti_launch_tool($instance, $foruserid=0) {list($endpoint, $parms) = lti_get_launch_data($instance, '', '', $foruserid);$debuglaunch = ( $instance->debuglaunch == 1 );$content = lti_post_launch_html($parms, $endpoint, $debuglaunch);echo $content;}/*** Prepares an LTI registration request message** @param object $toolproxy Tool Proxy instance object*/function lti_register($toolproxy) {$endpoint = $toolproxy->regurl;// Change the status to pending.$toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING;lti_update_tool_proxy($toolproxy);$requestparams = lti_build_registration_request($toolproxy);$content = lti_post_launch_html($requestparams, $endpoint, false);echo $content;}/*** Gets the parameters for the regirstration request** @param object $toolproxy Tool Proxy instance object* @return array Registration request parameters*/function lti_build_registration_request($toolproxy) {$key = $toolproxy->guid;$secret = $toolproxy->secret;$requestparams = array();$requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest';$requestparams['lti_version'] = 'LTI-2p0';$requestparams['reg_key'] = $key;$requestparams['reg_password'] = $secret;$requestparams['reg_url'] = $toolproxy->regurl;// Add the profile URL.$profileservice = lti_get_service_by_name('profile');$profileservice->set_tool_proxy($toolproxy);$requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url');// Add the return URL.$returnurlparams = array('id' => $toolproxy->id, 'sesskey' => sesskey());$url = new \moodle_url('/mod/lti/externalregistrationreturn.php', $returnurlparams);$returnurl = $url->out(false);$requestparams['launch_presentation_return_url'] = $returnurl;return $requestparams;}/** get Organization ID using default if no value provided* @param object $typeconfig* @return string*/function lti_get_organizationid($typeconfig) {global $CFG;// Default the organizationid if not specified.if (empty($typeconfig['organizationid'])) {if (($typeconfig['organizationid_default'] ?? LTI_DEFAULT_ORGID_SITEHOST) == LTI_DEFAULT_ORGID_SITEHOST) {$urlparts = parse_url($CFG->wwwroot);return $urlparts['host'];} else {return md5(get_site_identifier());}}return $typeconfig['organizationid'];}/*** Build source ID** @param int $instanceid* @param int $userid* @param string $servicesalt* @param null|int $typeid* @param null|int $launchid* @return stdClass*/function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) {$data = new \stdClass();$data->instanceid = $instanceid;$data->userid = $userid;$data->typeid = $typeid;if (!empty($launchid)) {$data->launchid = $launchid;} else {$data->launchid = mt_rand();}$json = json_encode($data);$hash = hash('sha256', $json . $servicesalt, false);$container = new \stdClass();$container->data = $data;$container->hash = $hash;return $container;}/*** This function builds the request that must be sent to the tool producer** @param object $instance Basic LTI instance object* @param array $typeconfig Basic LTI tool configuration* @param object $course Course object* @param int|null $typeid Basic LTI tool ID* @param boolean $islti2 True if an LTI 2 tool is being launched* @param string $messagetype LTI Message Type for this launch* @param int $foruserid User targeted by this launch** @return array Request details*/function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false,$messagetype = 'basic-lti-launch-request', $foruserid = 0) {global $USER, $CFG;if (empty($instance->cmid)) {$instance->cmid = 0;}$role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2);$requestparams = array('user_id' => $USER->id,'lis_person_sourcedid' => $USER->idnumber,'roles' => $role,'context_id' => $course->id,'context_label' => trim(html_to_text($course->shortname, 0)),'context_title' => trim(html_to_text($course->fullname, 0)),);if ($foruserid) {$requestparams['for_user_id'] = $foruserid;}if ($messagetype) {$requestparams['lti_message_type'] = $messagetype;}if (!empty($instance->name)) {$requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0));}if (!empty($instance->cmid)) {$intro = format_module_intro('lti', $instance, $instance->cmid);$intro = trim(html_to_text($intro, 0, false));// This may look weird, but this is required for new lines// so we generate the same OAuth signature as the tool provider.$intro = str_replace("\n", "\r\n", $intro);$requestparams['resource_link_description'] = $intro;}if (!empty($instance->id)) {$requestparams['resource_link_id'] = $instance->id;}if (!empty($instance->resource_link_id)) {$requestparams['resource_link_id'] = $instance->resource_link_id;}if ($course->format == 'site') {$requestparams['context_type'] = 'Group';} else {$requestparams['context_type'] = 'CourseSection';$requestparams['lis_course_section_sourcedid'] = $course->idnumber;}if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 ||$typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS ||($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))) {$placementsecret = $instance->servicesalt;$sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid));$requestparams['lis_result_sourcedid'] = $sourcedid;// Add outcome service URL.$serviceurl = new \moodle_url('/mod/lti/service.php');$serviceurl = $serviceurl->out();$forcessl = false;if (!empty($CFG->mod_lti_forcessl)) {$forcessl = true;}if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) {$serviceurl = lti_ensure_url_is_https($serviceurl);}$requestparams['lis_outcome_service_url'] = $serviceurl;}// Send user's name and email data if appropriate.if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS ||($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname)&& $instance->instructorchoicesendname == LTI_SETTING_ALWAYS)) {$requestparams['lis_person_name_given'] = $USER->firstname;$requestparams['lis_person_name_family'] = $USER->lastname;$requestparams['lis_person_name_full'] = fullname($USER);$requestparams['ext_user_username'] = $USER->username;}if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS ||($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr)&& $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS)) {$requestparams['lis_person_contact_email_primary'] = $USER->email;}return $requestparams;}/*** This function builds the request that must be sent to an LTI 2 tool provider** @param object $tool Basic LTI tool object* @param array $params Custom launch parameters** @return array Request details*/function lti_build_request_lti2($tool, $params) {$requestparams = array();$capabilities = lti_get_capabilities();$enabledcapabilities = explode("\n", $tool->enabledcapability);foreach ($enabledcapabilities as $capability) {if (array_key_exists($capability, $capabilities)) {$val = $capabilities[$capability];if ($val && (substr($val, 0, 1) != '$')) {if (isset($params[$val])) {$requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]];}}}}return $requestparams;}/*** This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer** @param stdClass $instance Basic LTI instance object* @param string $orgid Organisation ID* @param boolean $islti2 True if an LTI 2 tool is being launched* @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty.** @return array Request details* @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.* @see lti_build_standard_message()*/function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') {if (!$islti2) {$ltiversion = LTI_VERSION_1;} else {$ltiversion = LTI_VERSION_2;}return lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype);}/*** This function builds the standard parameters for an LTI message that must be sent to the tool producer** @param stdClass $instance Basic LTI instance object* @param string $orgid Organisation ID* @param boolean $ltiversion LTI version to be used for tool messages* @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty.** @return array Message parameters*/function lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype = 'basic-lti-launch-request') {global $CFG;$requestparams = array();if ($instance) {$requestparams['resource_link_id'] = $instance->id;if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) {$requestparams['resource_link_id'] = $instance->resource_link_id;}}$requestparams['launch_presentation_locale'] = current_language();// Make sure we let the tool know what LMS they are being called from.$requestparams['ext_lms'] = 'moodle-2';$requestparams['tool_consumer_info_product_family_code'] = 'moodle';$requestparams['tool_consumer_info_version'] = strval($CFG->version);// Add oauth_callback to be compliant with the 1.0A spec.$requestparams['oauth_callback'] = 'about:blank';$requestparams['lti_version'] = $ltiversion;$requestparams['lti_message_type'] = $messagetype;if ($orgid) {$requestparams["tool_consumer_instance_guid"] = $orgid;}if (!empty($CFG->mod_lti_institution_name)) {$requestparams['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0));} else {$requestparams['tool_consumer_instance_name'] = get_site()->shortname;}$requestparams['tool_consumer_instance_description'] = trim(html_to_text(get_site()->fullname, 0));return $requestparams;}/*** This function builds the custom parameters** @param object $toolproxy Tool proxy instance object* @param object $tool Tool instance object* @param object $instance Tool placement instance object* @param array $params LTI launch parameters* @param string $customstr Custom parameters defined for tool* @param string $instructorcustomstr Custom parameters defined for this placement* @param boolean $islti2 True if an LTI 2 tool is being launched** @return array Custom parameters*/function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) {// Concatenate the custom parameters from the administrator and the instructor// Instructor parameters are only taken into consideration if the administrator// has given permission.$custom = array();if ($customstr) {$custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2);}if ($instructorcustomstr) {$custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,$instructorcustomstr, $islti2), $custom);}if ($islti2) {$custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params,$tool->parameter, true), $custom);$settings = lti_get_tool_settings($tool->toolproxyid);$custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));if (!empty($instance->course)) {$settings = lti_get_tool_settings($tool->toolproxyid, $instance->course);$custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));if (!empty($instance->id)) {$settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id);$custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings));}}}return $custom;}/*** Builds a standard LTI Content-Item selection request.** @param int $id The tool type ID.* @param stdClass $course The course object.* @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP)* will use to return the Content-Item message.* @param string $title The tool's title, if available.* @param string $text The text to display to represent the content item. This value may be a long description of the content item.* @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default.* @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened* (via the presentationDocumentTarget element for a returned content item).* If empty, "frame", "iframe", and "window" will be supported by default.* @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without* @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default.* any option for the user to cancel the operation. False by default.* @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not.* A signed message should always be required when the content item is being created automatically in the* TC without further interaction from the user. False by default.* @param bool $canconfirm Flag for can_confirm parameter. False by default.* @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default.* @param string $nonce* @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface.* @throws moodle_exception When the LTI tool type does not exist.`* @throws coding_exception For invalid media type and presentation target parameters.*/function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],$presentationtargets = [], $autocreate = false, $multiple = true,$unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {global $USER;$tool = lti_get_type($id);// Validate parameters.if (!$tool) {throw new moodle_exception('errortooltypenotfound', 'mod_lti');}if (!is_array($mediatypes)) {throw new coding_exception('The list of accepted media types should be in an array');}if (!is_array($presentationtargets)) {throw new coding_exception('The list of accepted presentation targets should be in an array');}// Check title. If empty, use the tool's name.if (empty($title)) {$title = $tool->name;}$typeconfig = lti_get_type_config($id);$key = '';$secret = '';$islti2 = false;$islti13 = false;if (isset($tool->toolproxyid)) {$islti2 = true;$toolproxy = lti_get_tool_proxy($tool->toolproxyid);$key = $toolproxy->guid;$secret = $toolproxy->secret;} else {$islti13 = $tool->ltiversion === LTI_VERSION_1P3;$toolproxy = null;if ($islti13 && !empty($tool->clientid)) {$key = $tool->clientid;} else if (!$islti13 && !empty($typeconfig['resourcekey'])) {$key = $typeconfig['resourcekey'];}if (!empty($typeconfig['password'])) {$secret = $typeconfig['password'];}}$tool->enabledcapability = '';if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) {$tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest'];}$tool->parameter = '';if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) {$tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest'];}// Set the tool URL.if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) {$toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']);} else {$toolurl = new moodle_url($typeconfig['toolurl']);}// Check if SSL is forced.if (!empty($typeconfig['forcessl'])) {// Make sure the tool URL is set to https.if (strtolower($toolurl->get_scheme()) === 'http') {$toolurl->set_scheme('https');}// Make sure the return URL is set to https.if (strtolower($returnurl->get_scheme()) === 'http') {$returnurl->set_scheme('https');}}$toolurlout = $toolurl->out(false);// Get base request parameters.$instance = new stdClass();$instance->course = $course->id;$requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2);// Get LTI2-specific request parameters and merge to the request parameters if applicable.if ($islti2) {$lti2params = lti_build_request_lti2($tool, $requestparams);$requestparams = array_merge($requestparams, $lti2params);}// Get standard request parameters and merge to the request parameters.$orgid = lti_get_organizationid($typeconfig);$standardparams = lti_build_standard_message(null, $orgid, $tool->ltiversion, 'ContentItemSelectionRequest');$requestparams = array_merge($requestparams, $standardparams);// Get custom request parameters and merge to the request parameters.$customstr = '';if (!empty($typeconfig['customparameters'])) {$customstr = $typeconfig['customparameters'];}$customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2);$requestparams = array_merge($requestparams, $customparams);// Add the parameters configured by the LTI services.if ($id && !$islti2) {$services = lti_get_services();foreach ($services as $service) {$serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest',$course->id, $USER->id , $id);foreach ($serviceparameters as $paramkey => $paramvalue) {$requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,$islti2);}}}// Allow request params to be updated by sub-plugins.$plugins = core_component::get_plugin_list('ltisource');foreach (array_keys($plugins) as $plugin) {$pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []);if (!empty($pluginparams) && is_array($pluginparams)) {$requestparams = array_merge($requestparams, $pluginparams);}}if (!$islti13) {// Media types. Set to ltilink by default if empty.if (empty($mediatypes)) {$mediatypes = ['application/vnd.ims.lti.v1.ltilink',];}$requestparams['accept_media_types'] = implode(',', $mediatypes);} else {// Only LTI links are currently supported.$requestparams['accept_types'] = 'ltiResourceLink';}// Presentation targets. Supports frame, iframe, window by default if empty.if (empty($presentationtargets)) {$presentationtargets = ['frame','iframe','window',];}$requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets);// Other request parameters.$requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false';$requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false';$requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false';$requestparams['auto_create'] = $autocreate === true ? 'true' : 'false';$requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false';$requestparams['content_item_return_url'] = $returnurl->out(false);$requestparams['title'] = $title;$requestparams['text'] = $text;if (!$islti13) {$signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret);} else {$signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce);}$toolurlparams = $toolurl->params();// Strip querystring params in endpoint url from $signedparams to avoid duplication.if (!empty($toolurlparams) && !empty($signedparams)) {foreach (array_keys($toolurlparams) as $paramname) {if (isset($signedparams[$paramname])) {unset($signedparams[$paramname]);}}}// Check for params that should not be passed. Unset if they are set.$unwantedparams = ['resource_link_id','resource_link_title','resource_link_description','launch_presentation_return_url','lis_result_sourcedid',];foreach ($unwantedparams as $param) {if (isset($signedparams[$param])) {unset($signedparams[$param]);}}// Prepare result object.$result = new stdClass();$result->params = $signedparams;$result->url = $toolurlout;return $result;}/*** Verifies the OAuth signature of an incoming message.** @param int $typeid The tool type ID.* @param string $consumerkey The consumer key.* @return stdClass Tool type* @throws moodle_exception* @throws lti\OAuthException*/function lti_verify_oauth_signature($typeid, $consumerkey) {$tool = lti_get_type($typeid);// Validate parameters.if (!$tool) {throw new moodle_exception('errortooltypenotfound', 'mod_lti');}$typeconfig = lti_get_type_config($typeid);if (isset($tool->toolproxyid)) {$toolproxy = lti_get_tool_proxy($tool->toolproxyid);$key = $toolproxy->guid;$secret = $toolproxy->secret;} else {$toolproxy = null;if (!empty($typeconfig['resourcekey'])) {$key = $typeconfig['resourcekey'];} else {$key = '';}if (!empty($typeconfig['password'])) {$secret = $typeconfig['password'];} else {$secret = '';}}if ($consumerkey !== $key) {throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');}$store = new lti\TrivialOAuthDataStore();$store->add_consumer($key, $secret);$server = new lti\OAuthServer($store);$method = new lti\OAuthSignatureMethod_HMAC_SHA1();$server->add_signature_method($method);$request = lti\OAuthRequest::from_request();try {$server->verify_request($request);} catch (lti\OAuthException $e) {throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage());}return $tool;}/*** Verifies the JWT signature using a JWK keyset.** @param string $jwtparam JWT parameter value.* @param string $keyseturl The tool keyseturl.* @param string $clientid The tool client id.** @return object The JWT's payload as a PHP object* @throws moodle_exception* @throws UnexpectedValueException Provided JWT was invalid* @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed* @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'* @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'* @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim*/function lti_verify_with_keyset($jwtparam, $keyseturl, $clientid) {// Attempts to retrieve cached keyset.$cache = cache::make('mod_lti', 'keyset');$keyset = $cache->get($clientid);try {if (empty($keyset)) {throw new moodle_exception('errornocachedkeysetfound', 'mod_lti');}$keysetarr = json_decode($keyset, true);// JWK::parseKeySet uses RS256 algorithm by default.$keys = JWK::parseKeySet($keysetarr);$jwt = JWT::decode($jwtparam, $keys);} catch (Exception $e) {// Something went wrong, so attempt to update cached keyset and then try again.$keyset = download_file_content($keyseturl);$keysetarr = json_decode($keyset, true);// Fix for firebase/php-jwt's dependency on the optional 'alg' property in the JWK.$keysetarr = jwks_helper::fix_jwks_alg($keysetarr, $jwtparam);// JWK::parseKeySet uses RS256 algorithm by default.$keys = JWK::parseKeySet($keysetarr);$jwt = JWT::decode($jwtparam, $keys);// If sucessful, updates the cached keyset.$cache->set($clientid, $keyset);}return $jwt;}/*** Verifies the JWT signature of an incoming message.** @param int $typeid The tool type ID.* @param string $consumerkey The consumer key.* @param string $jwtparam JWT parameter value** @return stdClass Tool type* @throws moodle_exception* @throws UnexpectedValueException Provided JWT was invalid* @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed* @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'* @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'* @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim*/function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {$tool = lti_get_type($typeid);// Validate parameters.if (!$tool) {throw new moodle_exception('errortooltypenotfound', 'mod_lti');}if (isset($tool->toolproxyid)) {throw new moodle_exception('JWT security not supported with LTI 2');}$typeconfig = lti_get_type_config($typeid);$key = $tool->clientid ?? '';if ($consumerkey !== $key) {throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');}if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) {$publickey = $typeconfig['publickey'] ?? '';if (empty($publickey)) {throw new moodle_exception('No public key configured');}// Attemps to verify jwt with RSA key.JWT::decode($jwtparam, new Key($publickey, 'RS256'));} else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) {$keyseturl = $typeconfig['publickeyset'] ?? '';if (empty($keyseturl)) {throw new moodle_exception('No public keyset configured');}// Attempts to verify jwt with jwk keyset.lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid);} else {throw new moodle_exception('Invalid public key type');}return $tool;}/*** Converts an array of custom parameters to a new line separated string.** @param object $params list of params to concatenate** @return string*/function params_to_string(object $params) {$customparameters = [];foreach ($params as $key => $value) {$customparameters[] = "{$key}={$value}";}return implode("\n", $customparameters);}/*** Converts LTI 1.1 Content Item for LTI Link to Form data.** @param object $tool Tool for which the item is created for.* @param object $typeconfig The tool configuration.* @param object $item Item populated from JSON to be converted to Form form** @return stdClass Form config for the item*/function content_item_to_form(object $tool, object $typeconfig, object $item): stdClass {global $OUTPUT;$config = new stdClass();$config->name = '';if (isset($item->title)) {$config->name = $item->title;}if (empty($config->name)) {$config->name = $tool->name;}if (isset($item->text)) {$config->introeditor = ['text' => $item->text,'format' => FORMAT_PLAIN];} else {$config->introeditor = ['text' => '','format' => FORMAT_PLAIN];}if (isset($item->icon->{'@id'})) {$iconurl = new moodle_url($item->icon->{'@id'});// Assign item's icon URL to secureicon or icon depending on its scheme.if (strtolower($iconurl->get_scheme()) === 'https') {$config->secureicon = $iconurl->out(false);} else {$config->icon = $iconurl->out(false);}}if (isset($item->url)) {$url = new moodle_url($item->url);$config->toolurl = $url->out(false);$config->typeid = 0;} else {$config->typeid = $tool->id;}$config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;$islti2 = $tool->ltiversion === LTI_VERSION_2;if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {$acceptgrades = $typeconfig->lti_acceptgrades;if ($acceptgrades == LTI_SETTING_ALWAYS) {// We create a line item regardless if the definition contains one or not.$config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;$config->grade_modgrade_point = 100;}if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {if (isset($item->lineItem)) {$lineitem = $item->lineItem;$config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;$maxscore = 100;if (isset($lineitem->scoreConstraints)) {$sc = $lineitem->scoreConstraints;if (isset($sc->totalMaximum)) {$maxscore = $sc->totalMaximum;} else if (isset($sc->normalMaximum)) {$maxscore = $sc->normalMaximum;}}$config->grade_modgrade_point = $maxscore;$config->lineitemresourceid = '';$config->lineitemtag = '';$config->lineitemsubreviewurl = '';$config->lineitemsubreviewparams = '';if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {$config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';}if (isset($lineitem->tag)) {$config->lineitemtag = $lineitem->tag?:'';}if (isset($lineitem->submissionReview)) {$subreview = $lineitem->submissionReview;$config->lineitemsubreviewurl = 'DEFAULT';if (!empty($subreview->url)) {$config->lineitemsubreviewurl = $subreview->url;}if (isset($subreview->custom)) {$config->lineitemsubreviewparams = params_to_string($subreview->custom);}}}}}$config->instructorchoicesendname = LTI_SETTING_NEVER;$config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;// Since 4.3, the launch container is dictated by the value set in tool configuration and isn't controllable by content items.$config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;if (isset($item->custom)) {$config->instructorcustomparameters = params_to_string($item->custom);}// Pass an indicator to the relevant form field.$config->selectcontentindicator = $OUTPUT->pix_icon('i/valid', get_string('yes')) . get_string('contentselected', 'mod_lti');return $config;}/*** Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the* selected content item. This configuration data can be then used when adding a tool into the course.** @param int $typeid The tool type ID.* @param string $messagetype The value for the lti_message_type parameter.* @param string $ltiversion The value for the lti_version parameter.* @param string $consumerkey The consumer key.* @param string $contentitemsjson The JSON string for the content_items parameter.* @return stdClass The array of module information objects.* @throws moodle_exception* @throws lti\OAuthException*/function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {$tool = lti_get_type($typeid);// Validate parameters.if (!$tool) {throw new moodle_exception('errortooltypenotfound', 'mod_lti');}// Check lti_message_type. Show debugging if it's not set to ContentItemSelection.// No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.if ($messagetype !== 'ContentItemSelection') {debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",DEBUG_DEVELOPER);}// Check LTI versions from our side and the response's side. Show debugging if they don't match.// No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.$expectedversion = $tool->ltiversion;$islti2 = ($expectedversion === LTI_VERSION_2);if ($ltiversion !== $expectedversion) {debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," ." Response: {$ltiversion}", DEBUG_DEVELOPER);}$items = json_decode($contentitemsjson);if (empty($items)) {throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);}if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');}$config = null;$items = $items->{'@graph'};if (!empty($items)) {$typeconfig = lti_get_type_type_config($tool->id);if (count($items) == 1) {$config = content_item_to_form($tool, $typeconfig, $items[0]);} else {$multiple = [];foreach ($items as $item) {$multiple[] = content_item_to_form($tool, $typeconfig, $item);}$config = new stdClass();$config->multiple = $multiple;}}return $config;}/*** Converts the new Deep-Linking format for Content-Items to the old format.** @param string $param JSON string representing new Deep-Linking format* @return string JSON representation of content-items*/function lti_convert_content_items($param) {$items = array();$json = json_decode($param);if (!empty($json) && is_array($json)) {foreach ($json as $item) {if (isset($item->type)) {$newitem = clone $item;switch ($item->type) {case 'ltiResourceLink':$newitem->{'@type'} = 'LtiLinkItem';$newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink';break;case 'link':case 'rich':$newitem->{'@type'} = 'ContentItem';$newitem->mediaType = 'text/html';break;case 'file':$newitem->{'@type'} = 'FileItem';break;}unset($newitem->type);if (isset($item->html)) {$newitem->text = $item->html;unset($newitem->html);}if (isset($item->iframe)) {// DeepLinking allows multiple options to be declared as supported.// We favor iframe over new window if both are specified.$newitem->placementAdvice = new stdClass();$newitem->placementAdvice->presentationDocumentTarget = 'iframe';if (isset($item->iframe->width)) {$newitem->placementAdvice->displayWidth = $item->iframe->width;}if (isset($item->iframe->height)) {$newitem->placementAdvice->displayHeight = $item->iframe->height;}unset($newitem->iframe);unset($newitem->window);} else if (isset($item->window)) {$newitem->placementAdvice = new stdClass();$newitem->placementAdvice->presentationDocumentTarget = 'window';if (isset($item->window->targetName)) {$newitem->placementAdvice->windowTarget = $item->window->targetName;}if (isset($item->window->width)) {$newitem->placementAdvice->displayWidth = $item->window->width;}if (isset($item->window->height)) {$newitem->placementAdvice->displayHeight = $item->window->height;}unset($newitem->window);} else if (isset($item->presentation)) {// This may have been part of an early draft but is not in the final spec// so keeping it around for now in case it's actually been used.$newitem->placementAdvice = new stdClass();if (isset($item->presentation->documentTarget)) {$newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;}if (isset($item->presentation->windowTarget)) {$newitem->placementAdvice->windowTarget = $item->presentation->windowTarget;}if (isset($item->presentation->width)) {$newitem->placementAdvice->dislayWidth = $item->presentation->width;}if (isset($item->presentation->height)) {$newitem->placementAdvice->dislayHeight = $item->presentation->height;}unset($newitem->presentation);}if (isset($item->icon) && isset($item->icon->url)) {$newitem->icon->{'@id'} = $item->icon->url;unset($newitem->icon->url);}if (isset($item->thumbnail) && isset($item->thumbnail->url)) {$newitem->thumbnail->{'@id'} = $item->thumbnail->url;unset($newitem->thumbnail->url);}if (isset($item->lineItem)) {unset($newitem->lineItem);$newitem->lineItem = new stdClass();$newitem->lineItem->{'@type'} = 'LineItem';$newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore';if (isset($item->lineItem->label)) {$newitem->lineItem->label = $item->lineItem->label;}if (isset($item->lineItem->resourceId)) {$newitem->lineItem->assignedActivity = new stdClass();$newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId;}if (isset($item->lineItem->tag)) {$newitem->lineItem->tag = $item->lineItem->tag;}if (isset($item->lineItem->scoreMaximum)) {$newitem->lineItem->scoreConstraints = new stdClass();$newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits';$newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum;}if (isset($item->lineItem->submissionReview)) {$newitem->lineItem->submissionReview = $item->lineItem->submissionReview;}}$items[] = $newitem;}}}$newitems = new stdClass();$newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';$newitems->{'@graph'} = $items;return json_encode($newitems);}function lti_get_tool_table($tools, $id) {global $OUTPUT;$html = '';$typename = get_string('typename', 'lti');$baseurl = get_string('baseurl', 'lti');$action = get_string('action', 'lti');$createdon = get_string('createdon', 'lti');if (!empty($tools)) {$html .= "<div id=\"{$id}_tools_container\" style=\"margin-top:.5em;margin-bottom:.5em\"><table id=\"{$id}_tools\"><thead><tr><th>$typename</th><th>$baseurl</th><th>$createdon</th><th>$action</th></tr></thead>";foreach ($tools as $type) {$date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));$accept = get_string('accept', 'lti');$update = get_string('update', 'lti');$delete = get_string('delete', 'lti');if (empty($type->toolproxyid)) {$baseurl = new \moodle_url('/mod/lti/typessettings.php', array('action' => 'accept','id' => $type->id,'sesskey' => sesskey(),'tab' => $id));$ref = $type->baseurl;} else {$baseurl = new \moodle_url('/mod/lti/toolssettings.php', array('action' => 'accept','id' => $type->id,'sesskey' => sesskey(),'tab' => $id));$ref = $type->tpname;}$accepthtml = $OUTPUT->action_icon($baseurl,new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,array('title' => $accept, 'class' => 'editing_accept'));$deleteaction = 'delete';if ($type->state == LTI_TOOL_STATE_CONFIGURED) {$accepthtml = '';}if ($type->state != LTI_TOOL_STATE_REJECTED) {$deleteaction = 'reject';$delete = get_string('reject', 'lti');}$updateurl = clone($baseurl);$updateurl->param('action', 'update');$updatehtml = $OUTPUT->action_icon($updateurl,new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,array('title' => $update, 'class' => 'editing_update'));if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) {$deleteurl = clone($baseurl);$deleteurl->param('action', $deleteaction);$deletehtml = $OUTPUT->action_icon($deleteurl,new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,array('title' => $delete, 'class' => 'editing_delete'));} else {$deletehtml = '';}$html .= "<tr><td>{$type->name}</td><td>{$ref}</td><td>{$date}</td><td align=\"center\">{$accepthtml}{$updatehtml}{$deletehtml}</td></tr>";}$html .= '</table></div>';} else {$html .= get_string('no_' . $id, 'lti');}return $html;}/*** This function builds the tab for a category of tool proxies** @param object $toolproxies Tool proxy instance objects* @param string $id Category ID** @return string HTML for tab*/function lti_get_tool_proxy_table($toolproxies, $id) {global $OUTPUT;if (!empty($toolproxies)) {$typename = get_string('typename', 'lti');$url = get_string('registrationurl', 'lti');$action = get_string('action', 'lti');$createdon = get_string('createdon', 'lti');$html = <<< EOD<div id="{$id}_tool_proxies_container" style="margin-top: 0.5em; margin-bottom: 0.5em"><table id="{$id}_tool_proxies"><thead><tr><th>{$typename}</th><th>{$url}</th><th>{$createdon}</th><th>{$action}</th></tr></thead>EOD;foreach ($toolproxies as $toolproxy) {$date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig'));$accept = get_string('register', 'lti');$update = get_string('update', 'lti');$delete = get_string('delete', 'lti');$baseurl = new \moodle_url('/mod/lti/registersettings.php', array('action' => 'accept','id' => $toolproxy->id,'sesskey' => sesskey(),'tab' => $id));$registerurl = new \moodle_url('/mod/lti/register.php', array('id' => $toolproxy->id,'sesskey' => sesskey(),'tab' => 'tool_proxy'));$accepthtml = $OUTPUT->action_icon($registerurl,new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null,array('title' => $accept, 'class' => 'editing_accept'));$deleteaction = 'delete';if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) {$accepthtml = '';}if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) {$delete = get_string('cancel', 'lti');}$updateurl = clone($baseurl);$updateurl->param('action', 'update');$updatehtml = $OUTPUT->action_icon($updateurl,new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null,array('title' => $update, 'class' => 'editing_update'));$deleteurl = clone($baseurl);$deleteurl->param('action', $deleteaction);$deletehtml = $OUTPUT->action_icon($deleteurl,new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null,array('title' => $delete, 'class' => 'editing_delete'));$html .= <<< EOD<tr><td>{$toolproxy->name}</td><td>{$toolproxy->regurl}</td><td>{$date}</td><td align="center">{$accepthtml}{$updatehtml}{$deletehtml}</td></tr>EOD;}$html .= '</table></div>';} else {$html = get_string('no_' . $id, 'lti');}return $html;}/*** Extracts the enabled capabilities into an array, including those implicitly declared in a parameter** @param object $tool Tool instance object** @return array List of enabled capabilities*/function lti_get_enabled_capabilities($tool) {if (!isset($tool)) {return array();}if (!empty($tool->enabledcapability)) {$enabledcapabilities = explode("\n", $tool->enabledcapability);} else {$enabledcapabilities = array();}if (!empty($tool->parameter)) {$paramstr = str_replace("\r\n", "\n", $tool->parameter);$paramstr = str_replace("\n\r", "\n", $paramstr);$paramstr = str_replace("\r", "\n", $paramstr);$params = explode("\n", $paramstr);foreach ($params as $param) {$pos = strpos($param, '=');if (($pos === false) || ($pos < 1)) {continue;}$value = trim(core_text::substr($param, $pos + 1, strlen($param)));if (substr($value, 0, 1) == '$') {$value = substr($value, 1);if (!in_array($value, $enabledcapabilities)) {$enabledcapabilities[] = $value;}}}}return $enabledcapabilities;}/*** Splits the custom parameters** @param string $customstr String containing the parameters** @return array of custom parameters*/function lti_split_parameters($customstr) {$customstr = str_replace("\r\n", "\n", $customstr);$customstr = str_replace("\n\r", "\n", $customstr);$customstr = str_replace("\r", "\n", $customstr);$lines = explode("\n", $customstr); // Or should this split on "/[\n;]/"?$retval = array();foreach ($lines as $line) {$pos = strpos($line, '=');if ( $pos === false || $pos < 1 ) {continue;}$key = trim(core_text::substr($line, 0, $pos));$val = trim(core_text::substr($line, $pos + 1, strlen($line)));$retval[$key] = $val;}return $retval;}/*** Splits the custom parameters field to the various parameters** @param object $toolproxy Tool proxy instance object* @param object $tool Tool instance object* @param array $params LTI launch parameters* @param string $customstr String containing the parameters* @param boolean $islti2 True if an LTI 2 tool is being launched** @return array of custom parameters*/function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {$splitted = lti_split_parameters($customstr);$retval = array();foreach ($splitted as $key => $val) {$val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);$key2 = lti_map_keyname($key);$retval['custom_'.$key2] = $val;if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) {$retval['custom_'.$key] = $val;}}return $retval;}/*** Adds the custom parameters to an array** @param object $toolproxy Tool proxy instance object* @param object $tool Tool instance object* @param array $params LTI launch parameters* @param array $parameters Array containing the parameters** @return array Array of custom parameters*/function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {$retval = array();foreach ($parameters as $key => $val) {$key2 = lti_map_keyname($key);$val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true);$retval['custom_'.$key2] = $val;if ($key != $key2) {$retval['custom_'.$key] = $val;}}return $retval;}/*** Parse a custom parameter to replace any substitution variables** @param object $toolproxy Tool proxy instance object* @param object $tool Tool instance object* @param array $params LTI launch parameters* @param string $value Custom parameter value* @param boolean $islti2 True if an LTI 2 tool is being launched** @return string Parsed value of custom parameter*/function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {// This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var.global $USER, $COURSE;if ($value) {if (substr($value, 0, 1) == '\\') {$value = substr($value, 1);} else if (substr($value, 0, 1) == '$') {$value1 = substr($value, 1);$enabledcapabilities = lti_get_enabled_capabilities($tool);if (!$islti2 || in_array($value1, $enabledcapabilities)) {$capabilities = lti_get_capabilities();if (array_key_exists($value1, $capabilities)) {$val = $capabilities[$value1];if ($val) {if (substr($val, 0, 1) != '$') {$value = $params[$val];} else {$valarr = explode('->', substr($val, 1), 2);$value = "{${$valarr[0]}->{$valarr[1]}}";$value = str_replace('<br />' , ' ', $value);$value = str_replace('<br>' , ' ', $value);$value = format_string($value);}} else {$value = lti_calculate_custom_parameter($value1);}} else {$val = $value;$services = lti_get_services();foreach ($services as $service) {$service->set_tool_proxy($toolproxy);$service->set_type($tool);$value = $service->parse_value($val);if ($val != $value) {break;}}}}}}return $value;}/*** Calculates the value of a custom parameter that has not been specified earlier** @param string $value Custom parameter value** @return string Calculated value of custom parameter*/function lti_calculate_custom_parameter($value) {global $USER, $COURSE;switch ($value) {case 'Moodle.Person.userGroupIds':return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]);case 'Context.id.history':return implode(",", get_course_history($COURSE));case 'CourseSection.timeFrame.begin':if (empty($COURSE->startdate)) {return "";}$dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC'));return $dt->format(DateTime::ATOM);case 'CourseSection.timeFrame.end':if (empty($COURSE->enddate)) {return "";}$dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC'));return $dt->format(DateTime::ATOM);}return null;}/*** Build the history chain for this course using the course originalcourseid.** @param object $course course for which the history is returned.** @return array ids of the source course in ancestry order, immediate parent 1st.*/function get_course_history($course) {global $DB;$history = [];$parentid = $course->originalcourseid;while (!empty($parentid) && !in_array($parentid, $history)) {$history[] = $parentid;$parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid));}return $history;}/*** Used for building the names of the different custom parameters** @param string $key Parameter name* @param bool $tolower Do we want to convert the key into lower case?* @return string Processed name*/function lti_map_keyname($key, $tolower = true) {if ($tolower) {$newkey = '';$key = core_text::strtolower(trim($key));foreach (str_split($key) as $ch) {if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {$newkey .= $ch;} else {$newkey .= '_';}}} else {$newkey = $key;}return $newkey;}/*** Gets the IMS role string for the specified user and LTI course module.** @param mixed $user User object or user id* @param int $cmid The course module id of the LTI activity* @param int $courseid The course id of the LTI activity* @param boolean $islti2 True if an LTI 2 tool is being launched** @return string A role string suitable for passing with an LTI launch*/function lti_get_ims_role($user, $cmid, $courseid, $islti2) {$roles = array();if (empty($cmid)) {// If no cmid is passed, check if the user is a teacher in the course// This allows other modules to programmatically "fake" a launch without// a real LTI instance.$context = context_course::instance($courseid);if (has_capability('moodle/course:manageactivities', $context, $user)) {array_push($roles, 'Instructor');} else {array_push($roles, 'Learner');}} else {$context = context_module::instance($cmid);if (has_capability('mod/lti:manage', $context)) {array_push($roles, 'Instructor');} else {array_push($roles, 'Learner');}}if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) {// Make sure admins do not have the Learner role, then set admin role.$roles = array_diff($roles, array('Learner'));if (!$islti2) {array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');} else {array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator');}}return join(',', $roles);}/*** Returns configuration details for the tool** @param int $typeid Basic LTI tool typeid** @return array Tool Configuration*/function lti_get_type_config($typeid) {global $DB;$query = "SELECT name, valueFROM {lti_types_config}WHERE typeid = :typeid1UNION ALLSELECT 'toolurl' AS name, baseurl AS valueFROM {lti_types}WHERE id = :typeid2UNION ALLSELECT 'icon' AS name, icon AS valueFROM {lti_types}WHERE id = :typeid3UNION ALLSELECT 'secureicon' AS name, secureicon AS valueFROM {lti_types}WHERE id = :typeid4";$typeconfig = array();$configs = $DB->get_records_sql($query,array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid));if (!empty($configs)) {foreach ($configs as $config) {$typeconfig[$config->name] = $config->value;}}return $typeconfig;}function lti_get_tools_by_url($url, $state, $courseid = null) {$domain = lti_get_domain_from_url($url);return lti_get_tools_by_domain($domain, $state, $courseid);}function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {global $DB, $SITE;$statefilter = '';$coursefilter = '';if ($state) {$statefilter = 'AND t.state = :state';}if ($courseid && $courseid != $SITE->id) {$coursefilter = 'OR t.course = :courseid';}$coursecategory = $DB->get_field('course', 'category', ['id' => $courseid]);$query = "SELECT t.*FROM {lti_types} tLEFT JOIN {lti_types_categories} tc on t.id = tc.typeidWHERE t.tooldomain = :tooldomainAND (t.course = :siteid $coursefilter)$statefilterAND (tc.id IS NULL OR tc.categoryid = :categoryid)";return $DB->get_records_sql($query, ['courseid' => $courseid,'siteid' => $SITE->id,'tooldomain' => $domain,'state' => $state,'categoryid' => $coursecategory]);}/*** Returns all basicLTI tools configured by the administrator** @param int $course** @return array*/function lti_filter_get_types($course) {global $DB;if (!empty($course)) {$where = "WHERE t.course = :course";$params = array('course' => $course);} else {$where = '';$params = array();}$query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpnameFROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id{$where}";return $DB->get_records_sql($query, $params);}/*** Given an array of tools, filter them based on their state** @param array $tools An array of lti_types records* @param int $state One of the LTI_TOOL_STATE_* constants* @return array*/function lti_filter_tool_types(array $tools, $state) {$return = array();foreach ($tools as $key => $tool) {if ($tool->state == $state) {$return[$key] = $tool;}}return $return;}/*** Returns all lti types visible in this course** @deprecated since Moodle 4.3* @param int $courseid The id of the course to retieve types for* @param array $coursevisible options for 'coursevisible' field,* default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]* @return stdClass[] All the lti types visible in the given course*/function lti_get_lti_types_by_course($courseid, $coursevisible = null) {debugging(__FUNCTION__ . '() is deprecated. Please use \mod_lti\local\types_helper::get_lti_types_by_course() instead.',DEBUG_DEVELOPER);global $USER;return \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id, $coursevisible ?? []);}/*** Returns tool types for lti add instance and edit page** @return array Array of lti types*/function lti_get_types_for_add_instance() {global $COURSE, $USER;// Always return the 'manual' type option, despite manual config being deprecated, so that we have it for legacy instances.$types = [(object) ['name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null]];$preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($COURSE->id, $USER->id);foreach ($preconfiguredtypes as $type) {$types[$type->id] = $type;}return $types;}/*** Returns a list of configured types in the given course** @param int $courseid The id of the course to retieve types for* @param int $sectionreturn section to return to for forming the URLs* @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link*/function lti_get_configured_types($courseid, $sectionreturn = 0) {global $OUTPUT, $USER;$types = [];$preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id,[LTI_COURSEVISIBLE_ACTIVITYCHOOSER]);foreach ($preconfiguredtypes as $ltitype) {$type = new stdClass();$type->id = $ltitype->id;$type->modclass = MOD_CLASS_ACTIVITY;$type->name = 'lti_type_' . $ltitype->id;// Clean the name. We don't want tags here.$type->title = clean_param($ltitype->name, PARAM_NOTAGS);$trimmeddescription = trim($ltitype->description ?? '');if ($trimmeddescription != '') {// Clean the description. We don't want tags here.$type->help = clean_param($trimmeddescription, PARAM_NOTAGS);$type->helplink = get_string('modulename_shortcut_link', 'lti');}$iconurl = get_tool_type_icon_url($ltitype);$iconclass = '';if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) {// Do not filter the icon if it is not the default LTI activity icon.$iconclass = 'nofilter';}$type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]);$params = ['add' => 'lti','return' => 0,'course' => $courseid,'typeid' => $ltitype->id,];if (!is_null($sectionreturn)) {$params['sr'] = $sectionreturn;}$type->link = new moodle_url('/course/modedit.php', $params);$types[] = $type;}return $types;}function lti_get_domain_from_url($url) {$matches = array();if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) {return $matches[1];}}function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) {$possibletools = lti_get_tools_by_url($url, $state, $courseid);return lti_get_best_tool_by_url($url, $possibletools, $courseid);}function lti_get_url_thumbprint($url) {// Parse URL requires a schema otherwise everything goes into 'path'. Fixed 5.4.7 or later.if (preg_match('/https?:\/\//', $url) !== 1) {$url = 'http://'.$url;}$urlparts = parse_url(strtolower($url));if (!isset($urlparts['path'])) {$urlparts['path'] = '';}if (!isset($urlparts['query'])) {$urlparts['query'] = '';}if (!isset($urlparts['host'])) {$urlparts['host'] = '';}if (substr($urlparts['host'], 0, 4) === 'www.') {$urlparts['host'] = substr($urlparts['host'], 4);}$urllower = $urlparts['host'] . '/' . $urlparts['path'];if ($urlparts['query'] != '') {$urllower .= '?' . $urlparts['query'];}return $urllower;}function lti_get_best_tool_by_url($url, $tools, $courseid = null) {if (count($tools) === 0) {return null;}$urllower = lti_get_url_thumbprint($url);foreach ($tools as $tool) {$tool->_matchscore = 0;$toolbaseurllower = lti_get_url_thumbprint($tool->baseurl);if ($urllower === $toolbaseurllower) {// 100 points for exact thumbprint match.$tool->_matchscore += 100;} else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) {// 50 points if tool thumbprint starts with the base URL thumbprint.$tool->_matchscore += 50;}// Prefer course tools over site tools.if (!empty($courseid)) {// Minus 10 points for not matching the course id (global tools).if ($tool->course != $courseid) {$tool->_matchscore -= 10;}}}$bestmatch = array_reduce($tools, function($value, $tool) {if ($tool->_matchscore > $value->_matchscore) {return $tool;} else {return $value;}}, (object)array('_matchscore' => -1));// None of the tools are suitable for this URL.if ($bestmatch->_matchscore <= 0) {return null;}return $bestmatch;}function lti_get_shared_secrets_by_key($key) {global $DB;// Look up the shared secret for the specified key in both the types_config table (for configured tools)// And in the lti resource table for ad-hoc tools.$lti13 = LTI_VERSION_1P3;$query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS valueFROM {lti_types_config} t1JOIN {lti_types_config} t2 ON t1.typeid = t2.typeidJOIN {lti_types} type ON t2.typeid = type.idWHERE t1.name = 'resourcekey'AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1AND t2.name = 'password'AND type.state = :configured1AND type.ltiversion <> :ltiversionUNIONSELECT tp.secret AS valueFROM {lti_tool_proxies} tpJOIN {lti_types} t ON tp.id = t.toolproxyidWHERE tp.guid = :key2AND t.state = :configured2UNIONSELECT password AS valueFROM {lti}WHERE resourcekey = :key3";$sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13,'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));$values = array_map(function($item) {return $item->value;}, $sharedsecrets);// There should really only be one shared secret per key. But, we can't prevent// more than one getting entered. For instance, if the same key is used for two tool providers.return $values;}/*** Delete a Basic LTI configuration** @param int $id Configuration id*/function lti_delete_type($id) {global $DB;// We should probably just copy the launch URL to the tool instances in this case... using a single query./*$instances = $DB->get_records('lti', array('typeid' => $id));foreach ($instances as $instance) {$instance->typeid = 0;$DB->update_record('lti', $instance);}*/$DB->delete_records('lti_types', array('id' => $id));$DB->delete_records('lti_types_config', array('typeid' => $id));$DB->delete_records('lti_types_categories', array('typeid' => $id));}function lti_set_state_for_type($id, $state) {global $DB;$DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));}/*** Transforms a basic LTI object to an array** @param object $ltiobject Basic LTI object** @return array Basic LTI configuration details*/function lti_get_config($ltiobject) {$typeconfig = (array)$ltiobject;$additionalconfig = lti_get_type_config($ltiobject->typeid);$typeconfig = array_merge($typeconfig, $additionalconfig);return $typeconfig;}/**** Generates some of the tool configuration based on the instance details** @param int $id** @return object configuration**/function lti_get_type_config_from_instance($id) {global $DB;$instance = $DB->get_record('lti', array('id' => $id));$config = lti_get_config($instance);$type = new \stdClass();$type->lti_fix = $id;if (isset($config['toolurl'])) {$type->lti_toolurl = $config['toolurl'];}if (isset($config['instructorchoicesendname'])) {$type->lti_sendname = $config['instructorchoicesendname'];}if (isset($config['instructorchoicesendemailaddr'])) {$type->lti_sendemailaddr = $config['instructorchoicesendemailaddr'];}if (isset($config['instructorchoiceacceptgrades'])) {$type->lti_acceptgrades = $config['instructorchoiceacceptgrades'];}if (isset($config['instructorchoiceallowroster'])) {$type->lti_allowroster = $config['instructorchoiceallowroster'];}if (isset($config['instructorcustomparameters'])) {$type->lti_allowsetting = $config['instructorcustomparameters'];}return $type;}/*** Generates some of the tool configuration based on the admin configuration details** @param int $id** @return stdClass Configuration details*/function lti_get_type_type_config($id) {global $DB;$basicltitype = $DB->get_record('lti_types', array('id' => $id));$config = lti_get_type_config($id);$type = new \stdClass();$type->lti_typename = $basicltitype->name;$type->typeid = $basicltitype->id;$type->course = $basicltitype->course;$type->toolproxyid = $basicltitype->toolproxyid;$type->lti_toolurl = $basicltitype->baseurl;$type->lti_ltiversion = $basicltitype->ltiversion;$type->lti_clientid = $basicltitype->clientid;$type->lti_clientid_disabled = $type->lti_clientid;$type->lti_description = $basicltitype->description;$type->lti_parameters = $basicltitype->parameter;$type->lti_icon = $basicltitype->icon;$type->lti_secureicon = $basicltitype->secureicon;if (isset($config['resourcekey'])) {$type->lti_resourcekey = $config['resourcekey'];}if (isset($config['password'])) {$type->lti_password = $config['password'];}if (isset($config['publickey'])) {$type->lti_publickey = $config['publickey'];}if (isset($config['publickeyset'])) {$type->lti_publickeyset = $config['publickeyset'];}if (isset($config['keytype'])) {$type->lti_keytype = $config['keytype'];}if (isset($config['initiatelogin'])) {$type->lti_initiatelogin = $config['initiatelogin'];}if (isset($config['redirectionuris'])) {$type->lti_redirectionuris = $config['redirectionuris'];}if (isset($config['sendname'])) {$type->lti_sendname = $config['sendname'];}if (isset($config['instructorchoicesendname'])) {$type->lti_instructorchoicesendname = $config['instructorchoicesendname'];}if (isset($config['sendemailaddr'])) {$type->lti_sendemailaddr = $config['sendemailaddr'];}if (isset($config['instructorchoicesendemailaddr'])) {$type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr'];}if (isset($config['acceptgrades'])) {$type->lti_acceptgrades = $config['acceptgrades'];}if (isset($config['instructorchoiceacceptgrades'])) {$type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades'];}if (isset($config['allowroster'])) {$type->lti_allowroster = $config['allowroster'];}if (isset($config['instructorchoiceallowroster'])) {$type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster'];}if (isset($config['customparameters'])) {$type->lti_customparameters = $config['customparameters'];}if (isset($config['forcessl'])) {$type->lti_forcessl = $config['forcessl'];}if (isset($config['organizationid_default'])) {$type->lti_organizationid_default = $config['organizationid_default'];} else {// Tool was configured before this option was available and the default then was host.$type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST;}if (isset($config['organizationid'])) {$type->lti_organizationid = $config['organizationid'];}if (isset($config['organizationurl'])) {$type->lti_organizationurl = $config['organizationurl'];}if (isset($config['organizationdescr'])) {$type->lti_organizationdescr = $config['organizationdescr'];}if (isset($config['launchcontainer'])) {$type->lti_launchcontainer = $config['launchcontainer'];}if (isset($config['coursevisible'])) {$type->lti_coursevisible = $config['coursevisible'];}if (isset($config['contentitem'])) {$type->lti_contentitem = $config['contentitem'];}if (isset($config['toolurl_ContentItemSelectionRequest'])) {$type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];}if (isset($config['debuglaunch'])) {$type->lti_debuglaunch = $config['debuglaunch'];}if (isset($config['module_class_type'])) {$type->lti_module_class_type = $config['module_class_type'];}// Get the parameters from the LTI services.foreach ($config as $name => $value) {if (strpos($name, 'ltiservice_') === 0) {$type->{$name} = $config[$name];}}return $type;}function lti_prepare_type_for_save($type, $config) {if (isset($config->lti_toolurl)) {$type->baseurl = $config->lti_toolurl;if (isset($config->lti_tooldomain)) {$type->tooldomain = $config->lti_tooldomain;} else {$type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);}}if (isset($config->lti_description)) {$type->description = $config->lti_description;}if (isset($config->lti_typename)) {$type->name = $config->lti_typename;}if (isset($config->lti_ltiversion)) {$type->ltiversion = $config->lti_ltiversion;}if (isset($config->lti_clientid)) {$type->clientid = $config->lti_clientid;}if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {$type->clientid = registration_helper::get()->new_clientid();} else if (empty($type->clientid)) {$type->clientid = null;}if (isset($config->lti_coursevisible)) {$type->coursevisible = $config->lti_coursevisible;}if (isset($config->lti_icon)) {$type->icon = $config->lti_icon;}if (isset($config->lti_secureicon)) {$type->secureicon = $config->lti_secureicon;}$type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0;$config->lti_forcessl = $type->forcessl;if (isset($config->lti_contentitem)) {$type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;$config->lti_contentitem = $type->contentitem;}if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {$type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;} else {$type->toolurl_ContentItemSelectionRequest = '';}$config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;}$type->timemodified = time();unset ($config->lti_typename);unset ($config->lti_toolurl);unset ($config->lti_description);unset ($config->lti_ltiversion);unset ($config->lti_clientid);unset ($config->lti_icon);unset ($config->lti_secureicon);}function lti_update_type($type, $config) {global $DB, $CFG;lti_prepare_type_for_save($type, $config);if (lti_request_is_using_ssl() && !empty($type->secureicon)) {$clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);} else {$clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon));}unset($config->oldicon);if ($DB->update_record('lti_types', $type)) {foreach ($config as $key => $value) {if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {$record = new \StdClass();$record->typeid = $type->id;$record->name = substr($key, 4);$record->value = $value;lti_update_config($record);}if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {$record = new \StdClass();$record->typeid = $type->id;$record->name = $key;$record->value = $value;lti_update_config($record);}}if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) {// We need to remove the tool proxy for this tool to function under 1.3.$toolproxyid = $type->toolproxyid;$DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid));$DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid));$type->toolproxyid = null;$DB->update_record('lti_types', $type);}$DB->delete_records('lti_types_categories', ['typeid' => $type->id]);if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) {lti_type_add_categories($type->id, $config->lti_coursecategories);}require_once($CFG->libdir.'/modinfolib.php');if ($clearcache) {$sql = "SELECT cm.id, cm.courseFROM {course_modules} cmJOIN {modules} m ON cm.module = m.idJOIN {lti} l ON l.course = cm.courseWHERE m.name = :name AND l.typeid = :typeid";$rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]);$courseids = [];foreach ($rs as $record) {$courseids[] = $record->course;\course_modinfo::purge_course_module_cache($record->course, $record->id);}$rs->close();$courseids = array_unique($courseids);foreach ($courseids as $courseid) {rebuild_course_cache($courseid, false, true);}}}}/*** Add LTI Type course category.** @param int $typeid* @param string $lticoursecategories Comma separated list of course categories.* @return void*/function lti_type_add_categories(int $typeid, string $lticoursecategories = ''): void {global $DB;$coursecategories = explode(',', $lticoursecategories);foreach ($coursecategories as $coursecategory) {$DB->insert_record('lti_types_categories', ['typeid' => $typeid, 'categoryid' => $coursecategory]);}}function lti_add_type($type, $config) {global $USER, $SITE, $DB;lti_prepare_type_for_save($type, $config);if (!isset($type->state)) {$type->state = LTI_TOOL_STATE_PENDING;}if (!isset($type->ltiversion)) {$type->ltiversion = LTI_VERSION_1;}if (!isset($type->timecreated)) {$type->timecreated = time();}if (!isset($type->createdby)) {$type->createdby = $USER->id;}if (!isset($type->course)) {$type->course = $SITE->id;}// Create a salt value to be used for signing passed data to extension services// The outcome service uses the service salt on the instance. This can be used// for communication with services not related to a specific LTI instance.$config->lti_servicesalt = uniqid('', true);$id = $DB->insert_record('lti_types', $type);if ($id) {foreach ($config as $key => $value) {if (!is_null($value)) {if (substr($key, 0, 4) === 'lti_') {$fieldname = substr($key, 4);} else if (substr($key, 0, 11) !== 'ltiservice_') {continue;} else {$fieldname = $key;}$record = new \StdClass();$record->typeid = $id;$record->name = $fieldname;$record->value = $value;lti_add_config($record);}}if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) {lti_type_add_categories($id, $config->lti_coursecategories);}}return $id;}/*** Given an array of tool proxies, filter them based on their state** @param array $toolproxies An array of lti_tool_proxies records* @param int $state One of the LTI_TOOL_PROXY_STATE_* constants** @return array*/function lti_filter_tool_proxy_types(array $toolproxies, $state) {$return = array();foreach ($toolproxies as $key => $toolproxy) {if ($toolproxy->state == $state) {$return[$key] = $toolproxy;}}return $return;}/*** Get the tool proxy instance given its GUID** @param string $toolproxyguid Tool proxy GUID value** @return object*/function lti_get_tool_proxy_from_guid($toolproxyguid) {global $DB;$toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid));return $toolproxy;}/*** Get the tool proxy instance given its registration URL** @param string $regurl Tool proxy registration URL** @return array The record of the tool proxy with this url*/function lti_get_tool_proxies_from_registration_url($regurl) {global $DB;return $DB->get_records_sql('SELECT * FROM {lti_tool_proxies}WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl',array('regurl' => $regurl));}/*** Generates some of the tool proxy configuration based on the admin configuration details** @param int $id** @return mixed Tool Proxy details*/function lti_get_tool_proxy($id) {global $DB;$toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id));return $toolproxy;}/*** Returns lti tool proxies.** @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them* @return array of basicLTI types*/function lti_get_tool_proxies($orphanedonly) {global $DB;if ($orphanedonly) {$usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));$proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');foreach ($proxies as $key => $value) {if (in_array($value->id, $usedproxyids)) {unset($proxies[$key]);}}return $proxies;} else {return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');}}/*** Generates some of the tool proxy configuration based on the admin configuration details** @param int $id** @return mixed Tool Proxy details*/function lti_get_tool_proxy_config($id) {$toolproxy = lti_get_tool_proxy($id);$tp = new \stdClass();$tp->lti_registrationname = $toolproxy->name;$tp->toolproxyid = $toolproxy->id;$tp->state = $toolproxy->state;$tp->lti_registrationurl = $toolproxy->regurl;$tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered);$tp->lti_services = explode("\n", $toolproxy->serviceoffered);return $tp;}/*** Update the database with a tool proxy instance** @param object $config Tool proxy definition** @return int Record id number*/function lti_add_tool_proxy($config) {global $USER, $DB;$toolproxy = new \stdClass();if (isset($config->lti_registrationname)) {$toolproxy->name = trim($config->lti_registrationname);}if (isset($config->lti_registrationurl)) {$toolproxy->regurl = trim($config->lti_registrationurl);}if (isset($config->lti_capabilities)) {$toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities);} else {$toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities()));}if (isset($config->lti_services)) {$toolproxy->serviceoffered = implode("\n", $config->lti_services);} else {$func = function($s) {return $s->get_id();};$servicenames = array_map($func, lti_get_services());$toolproxy->serviceoffered = implode("\n", $servicenames);}if (isset($config->toolproxyid) && !empty($config->toolproxyid)) {$toolproxy->id = $config->toolproxyid;if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) {$toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;$toolproxy->guid = random_string();$toolproxy->secret = random_string();}$id = lti_update_tool_proxy($toolproxy);} else {$toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED;$toolproxy->timemodified = time();$toolproxy->timecreated = $toolproxy->timemodified;if (!isset($toolproxy->createdby)) {$toolproxy->createdby = $USER->id;}$toolproxy->guid = random_string();$toolproxy->secret = random_string();$id = $DB->insert_record('lti_tool_proxies', $toolproxy);}return $id;}/*** Updates a tool proxy in the database** @param object $toolproxy Tool proxy** @return int Record id number*/function lti_update_tool_proxy($toolproxy) {global $DB;$toolproxy->timemodified = time();$id = $DB->update_record('lti_tool_proxies', $toolproxy);return $id;}/*** Delete a Tool Proxy** @param int $id Tool Proxy id*/function lti_delete_tool_proxy($id) {global $DB;$DB->delete_records('lti_tool_settings', array('toolproxyid' => $id));$tools = $DB->get_records('lti_types', array('toolproxyid' => $id));foreach ($tools as $tool) {lti_delete_type($tool->id);}$DB->delete_records('lti_tool_proxies', array('id' => $id));}/*** Get both LTI tool proxies and tool types.** If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool* types.* For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types* will be returned.** @param int $limit Maximum number of tools returned.* @param int $offset Do not return tools before offset index.* @param bool $orphanedonly If true, only return orphaned proxies.* @param int $toolproxyid If not 0, only return tool types that have this tool proxy id.* @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types.*/function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array {global $DB;if ($orphanedonly) {$orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false);$countsql = helper::get_tool_proxy_sql($orphanedonly, true);$proxies = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit);$totalproxiescount = $DB->count_records_sql($countsql);} else {$proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC','*', $offset, $limit);$totalproxiescount = $DB->count_records('lti_tool_proxies');}// Find new offset and limit for tool types after getting proxies and set up query.$typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative.$typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative.$typesparams = [];if (!empty($toolproxyid)) {$typesparams['toolproxyid'] = $toolproxyid;}$types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC','*', $typesoffset, $typeslimit);return [$proxies, array_map('serialise_tool_type', $types)];}/*** Get the total number of LTI tool types and tool proxies.** @param bool $orphanedonly If true, only count orphaned proxies.* @param int $toolproxyid If not 0, only count tool types that have this tool proxy id.* @return int Count of tools.*/function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int {global $DB;$typessql = "SELECT count(*)FROM {lti_types}";$typesparams = [];if (!empty($toolproxyid)) {$typessql .= " WHERE toolproxyid = :toolproxyid";$typesparams['toolproxyid'] = $toolproxyid;}$proxiessql = helper::get_tool_proxy_sql($orphanedonly, true);$countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause();return $DB->count_records_sql($countsql, $typesparams);}/*** Add a tool configuration in the database** @param object $config Tool configuration** @return int Record id number*/function lti_add_config($config) {global $DB;return $DB->insert_record('lti_types_config', $config);}/*** Updates a tool configuration in the database** @param object $config Tool configuration** @return mixed Record id number*/function lti_update_config($config) {global $DB;$old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));if ($old) {$config->id = $old->id;$return = $DB->update_record('lti_types_config', $config);} else {$return = $DB->insert_record('lti_types_config', $config);}return $return;}/*** Gets the tool settings** @param int $toolproxyid Id of tool proxy record (or tool ID if negative)* @param int $courseid Id of course (null if system settings)* @param int $instanceid Id of course module (null if system or context settings)** @return array Array settings*/function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) {global $DB;$settings = array();if ($toolproxyid > 0) {$settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,'course' => $courseid, 'coursemoduleid' => $instanceid));} else {$settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid,'course' => $courseid, 'coursemoduleid' => $instanceid));}if ($settingsstr !== false) {$settings = json_decode($settingsstr, true);}return $settings;}/*** Sets the tool settings (** @param array $settings Array of settings* @param int $toolproxyid Id of tool proxy record (or tool ID if negative)* @param int $courseid Id of course (null if system settings)* @param int $instanceid Id of course module (null if system or context settings)*/function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) {global $DB;$json = json_encode($settings);if ($toolproxyid >= 0) {$record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,'course' => $courseid, 'coursemoduleid' => $instanceid));} else {$record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid,'course' => $courseid, 'coursemoduleid' => $instanceid));}if ($record !== false) {$DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));} else {$record = new \stdClass();if ($toolproxyid > 0) {$record->toolproxyid = $toolproxyid;} else {$record->typeid = -$toolproxyid;}$record->course = $courseid;$record->coursemoduleid = $instanceid;$record->settings = $json;$record->timecreated = time();$record->timemodified = $record->timecreated;$DB->insert_record('lti_tool_settings', $record);}}/*** Signs the petition to launch the external tool using OAuth** @param array $oldparms Parameters to be passed for signing* @param string $endpoint url of the external tool* @param string $method Method for sending the parameters (e.g. POST)* @param string $oauthconsumerkey* @param string $oauthconsumersecret* @return array|null*/function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {$parms = $oldparms;$testtoken = '';// TODO: Switch to core oauthlib once implemented - MDL-30149.$hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1();$testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null);$accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms);$accreq->sign_request($hmacmethod, $testconsumer, $testtoken);$newparms = $accreq->get_parameters();return $newparms;}/*** Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT** @param array $parms Parameters to be passed for signing* @param string $endpoint url of the external tool* @param string $oauthconsumerkey* @param string $typeid ID of LTI tool type* @param string $nonce Nonce value to use* @return array|null*/function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {global $CFG;if (empty($typeid)) {$typeid = 0;}$messagetypemapping = lti_get_jwt_message_type_mapping();if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) {$parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']];}if (isset($parms['roles'])) {$roles = explode(',', $parms['roles']);$newroles = array();foreach ($roles as $role) {if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {$role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);} else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {$role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);} else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {$role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);} else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {$role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";}$newroles[] = $role;}$parms['roles'] = implode(',', $newroles);}$now = time();if (empty($nonce)) {$nonce = bin2hex(openssl_random_pseudo_bytes(10));}$claimmapping = lti_get_jwt_claim_mapping();$payload = array('nonce' => $nonce,'iat' => $now,'exp' => $now + 60,);$payload['iss'] = $CFG->wwwroot;$payload['aud'] = $oauthconsumerkey;$payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid);$payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;foreach ($parms as $key => $value) {$claim = LTI_JWT_CLAIM_PREFIX;if (array_key_exists($key, $claimmapping)) {$mapping = $claimmapping[$key];$type = $mapping["type"] ?? "string";if ($mapping['isarray']) {$value = explode(',', $value);sort($value);} else if ($type == 'boolean') {$value = isset($value) && ($value == 'true');}if (!empty($mapping['suffix'])) {$claim .= "-{$mapping['suffix']}";}$claim .= '/claim/';if (is_null($mapping['group'])) {$payload[$mapping['claim']] = $value;} else if (empty($mapping['group'])) {$payload["{$claim}{$mapping['claim']}"] = $value;} else {$claim .= $mapping['group'];$payload[$claim][$mapping['claim']] = $value;}} else if (strpos($key, 'custom_') === 0) {$payload["{$claim}/claim/custom"][substr($key, 7)] = $value;} else if (strpos($key, 'ext_') === 0) {$payload["{$claim}/claim/ext"][substr($key, 4)] = $value;}}$privatekey = jwks_helper::get_private_key();$jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']);$newparms = array();$newparms['id_token'] = $jwt;return $newparms;}/*** Verfies the JWT and converts its claims to their equivalent message parameter.** @param int $typeid* @param string $jwtparam JWT parameter** @return array message parameters* @throws moodle_exception*/function lti_convert_from_jwt($typeid, $jwtparam) {$params = array();$parts = explode('.', $jwtparam);$ok = (count($parts) === 3);if ($ok) {$payload = JWT::urlsafeB64Decode($parts[1]);$claims = json_decode($payload, true);$ok = !is_null($claims) && !empty($claims['iss']);}if ($ok) {lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);$params['oauth_consumer_key'] = $claims['iss'];foreach (lti_get_jwt_claim_mapping() as $key => $mapping) {$claim = LTI_JWT_CLAIM_PREFIX;if (!empty($mapping['suffix'])) {$claim .= "-{$mapping['suffix']}";}$claim .= '/claim/';if (is_null($mapping['group'])) {$claim = $mapping['claim'];} else if (empty($mapping['group'])) {$claim .= $mapping['claim'];} else {$claim .= $mapping['group'];}if (isset($claims[$claim])) {$value = null;if (empty($mapping['group'])) {$value = $claims[$claim];} else {$group = $claims[$claim];if (is_array($group) && array_key_exists($mapping['claim'], $group)) {$value = $group[$mapping['claim']];}}if (!empty($value) && $mapping['isarray']) {if (is_array($value)) {if (is_array($value[0])) {$value = json_encode($value);} else {$value = implode(',', $value);}}}if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {$params[$key] = $value;}}$claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';if (isset($claims[$claim])) {$custom = $claims[$claim];if (is_array($custom)) {foreach ($custom as $key => $value) {$params["custom_{$key}"] = $value;}}}$claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';if (isset($claims[$claim])) {$ext = $claims[$claim];if (is_array($ext)) {foreach ($ext as $key => $value) {$params["ext_{$key}"] = $value;}}}}}if (isset($params['content_items'])) {$params['content_items'] = lti_convert_content_items($params['content_items']);}$messagetypemapping = lti_get_jwt_message_type_mapping();if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) {$params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']];}return $params;}/*** Posts the launch petition HTML** @param array $newparms Signed parameters* @param string $endpoint URL of the external tool* @param bool $debug Debug (true/false)* @return string*/function lti_post_launch_html($newparms, $endpoint, $debug=false) {$r = "<form action=\"" . $endpoint ."\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n";// Contruct html for the launch parameters.foreach ($newparms as $key => $value) {$key = htmlspecialchars($key, ENT_COMPAT);$value = htmlspecialchars($value, ENT_COMPAT);if ( $key == "ext_submit" ) {$r .= "<input type=\"submit\"";} else {$r .= "<input type=\"hidden\" name=\"{$key}\"";}$r .= " value=\"";$r .= $value;$r .= "\"/>\n";}if ( $debug ) {$r .= "<script language=\"javascript\"> \n";$r .= " //<![CDATA[ \n";$r .= "function basicltiDebugToggle() {\n";$r .= " var ele = document.getElementById(\"basicltiDebug\");\n";$r .= " if (ele.style.display == \"block\") {\n";$r .= " ele.style.display = \"none\";\n";$r .= " }\n";$r .= " else {\n";$r .= " ele.style.display = \"block\";\n";$r .= " }\n";$r .= "} \n";$r .= " //]]> \n";$r .= "</script>\n";$r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";$r .= get_string("toggle_debug_data", "lti")."</a>\n";$r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";$r .= "<b>".get_string("basiclti_endpoint", "lti")."</b><br/>\n";$r .= $endpoint . "<br/>\n <br/>\n";$r .= "<b>".get_string("basiclti_parameters", "lti")."</b><br/>\n";foreach ($newparms as $key => $value) {$key = htmlspecialchars($key, ENT_COMPAT);$value = htmlspecialchars($value, ENT_COMPAT);$r .= "$key = $value<br/>\n";}$r .= " <br/>\n";$r .= "</div>\n";}$r .= "</form>\n";// Auto-submit the form if endpoint is set.if ($endpoint !== '' && !$debug) {$r .= " <script type=\"text/javascript\"> \n" ." //<![CDATA[ \n" ." document.ltiLaunchForm.submit(); \n" ." //]]> \n" ." </script> \n";}return $r;}/*** Generate the form for initiating a login request for an LTI 1.3 message** @param int $courseid Course ID* @param int $cmid LTI instance ID* @param stdClass|null $instance LTI instance* @param stdClass $config Tool type configuration* @param string $messagetype LTI message type* @param string $title Title of content item* @param string $text Description of content item* @param int $foruserid Id of the user targeted by the launch* @return string*/function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request',$title = '', $text = '', $foruserid = 0) {global $SESSION;$params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text);$r = "<form action=\"" . $config->lti_initiatelogin ."\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " ."encType=\"application/x-www-form-urlencoded\">\n";foreach ($params as $key => $value) {$key = htmlspecialchars($key, ENT_COMPAT);$value = htmlspecialchars($value, ENT_COMPAT);$r .= " <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";}$r .= "</form>\n";$r .= "<script type=\"text/javascript\">\n" ."//<![CDATA[\n" ."document.ltiInitiateLoginForm.submit();\n" ."//]]>\n" ."</script>\n";return $r;}/*** Prepares an LTI 1.3 login request** @param int $courseid Course ID* @param int $cmid Course Module instance ID* @param stdClass|null $instance LTI instance* @param stdClass $config Tool type configuration* @param string $messagetype LTI message type* @param int $foruserid Id of the user targeted by the launch* @param string $title Title of content item* @param string $text Description of content item* @return array Login request parameters*/function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') {global $USER, $CFG, $SESSION;$ltihint = [];if (!empty($instance)) {$endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;$launchid = 'ltilaunch'.$instance->id.'_'.rand();$ltihint['cmid'] = $cmid;$SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,";} else {$endpoint = $config->lti_toolurl;if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) {$endpoint = $config->lti_toolurl_ContentItemSelectionRequest;}$launchid = "ltilaunch_$messagetype".rand();$SESSION->$launchid ="{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text);}$endpoint = trim($endpoint);$services = lti_get_services();foreach ($services as $service) {[$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance);}$ltihint['launchid'] = $launchid;// If SSL is forced make sure https is on the normal launch URL.if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {$endpoint = lti_ensure_url_is_https($endpoint);} else if (!strstr($endpoint, '://')) {$endpoint = 'http://' . $endpoint;}$params = array();$params['iss'] = $CFG->wwwroot;$params['target_link_uri'] = $endpoint;$params['login_hint'] = $USER->id;$params['lti_message_hint'] = json_encode($ltihint);$params['client_id'] = $config->lti_clientid;$params['lti_deployment_id'] = $config->typeid;return $params;}function lti_get_type($typeid) {global $DB;return $DB->get_record('lti_types', array('id' => $typeid));}function lti_get_launch_container($lti, $toolconfig) {if (empty($lti->launchcontainer)) {$lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;}if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {if (isset($toolconfig['launchcontainer'])) {$launchcontainer = $toolconfig['launchcontainer'];}} else {$launchcontainer = $lti->launchcontainer;}if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) {$launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;}$devicetype = core_useragent::get_device_type();// Scrolling within the object element doesn't work on iOS or Android// Opening the popup window also had some issues in testing// For mobile devices, always take up the entire screen to ensure the best experience.if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) {$launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW;}return $launchcontainer;}function lti_request_is_using_ssl() {global $CFG;return (stripos($CFG->wwwroot, 'https://') === 0);}function lti_ensure_url_is_https($url) {if (!strstr($url, '://')) {$url = 'https://' . $url;} else {// If the URL starts with http, replace with https.if (stripos($url, 'http://') === 0) {$url = 'https://' . substr($url, 7);}}return $url;}/*** Determines if we should try to log the request** @param string $rawbody* @return bool*/function lti_should_log_request($rawbody) {global $CFG;if (empty($CFG->mod_lti_log_users)) {return false;}$logusers = explode(',', $CFG->mod_lti_log_users);if (empty($logusers)) {return false;}try {$xml = new \SimpleXMLElement($rawbody);$ns = $xml->getNamespaces();$ns = array_shift($ns);$xml->registerXPathNamespace('lti', $ns);$requestuserid = '';if ($node = $xml->xpath('//lti:userId')) {$node = $node[0];$requestuserid = clean_param((string) $node, PARAM_INT);} else if ($node = $xml->xpath('//lti:sourcedId')) {$node = $node[0];$resultjson = json_decode((string) $node);$requestuserid = clean_param($resultjson->data->userid, PARAM_INT);}} catch (Exception $e) {return false;}if (empty($requestuserid) or !in_array($requestuserid, $logusers)) {return false;}return true;}/*** Logs the request to a file in temp dir.** @param string $rawbody*/function lti_log_request($rawbody) {if ($tempdir = make_temp_directory('mod_lti', false)) {if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {$content = "Request Headers:\n";foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {$content .= "$header: $value\n";}$content .= "Request Body:\n";$content .= $rawbody;file_put_contents($tempfile, $content);chmod($tempfile, 0644);}}}/*** Log an LTI response.** @param string $responsexml The response XML* @param Exception $e If there was an exception, pass that too*/function lti_log_response($responsexml, $e = null) {if ($tempdir = make_temp_directory('mod_lti', false)) {if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {$content = '';if ($e instanceof Exception) {$info = get_exception_info($e);$content .= "Exception:\n";$content .= "Message: $info->message\n";$content .= "Debug info: $info->debuginfo\n";$content .= "Backtrace:\n";$content .= format_backtrace($info->backtrace, true);$content .= "\n";}$content .= "Response XML:\n";$content .= $responsexml;file_put_contents($tempfile, $content);chmod($tempfile, 0644);}}}/*** Fetches LTI type configuration for an LTI instance** @param stdClass $instance* @return array Can be empty if no type is found*/function lti_get_type_config_by_instance($instance) {$typeid = null;if (empty($instance->typeid)) {$tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);if ($tool) {$typeid = $tool->id;}} else {$typeid = $instance->typeid;}if (!empty($typeid)) {return lti_get_type_config($typeid);}return array();}/*** Enforce type config settings onto the LTI instance** @param stdClass $instance* @param array $typeconfig*/function lti_force_type_config_settings($instance, array $typeconfig) {$forced = array('instructorchoicesendname' => 'sendname','instructorchoicesendemailaddr' => 'sendemailaddr','instructorchoiceacceptgrades' => 'acceptgrades',);foreach ($forced as $instanceparam => $typeconfigparam) {if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) {$instance->$instanceparam = $typeconfig[$typeconfigparam];}}}/*** Initializes an array with the capabilities supported by the LTI module** @return array List of capability names (without a dollar sign prefix)*/function lti_get_capabilities() {$capabilities = array('basic-lti-launch-request' => '','ContentItemSelectionRequest' => '','ToolProxyRegistrationRequest' => '','Context.id' => 'context_id','Context.title' => 'context_title','Context.label' => 'context_label','Context.id.history' => null,'Context.sourcedId' => 'lis_course_section_sourcedid','Context.longDescription' => '$COURSE->summary','Context.timeFrame.begin' => '$COURSE->startdate','CourseSection.title' => 'context_title','CourseSection.label' => 'context_label','CourseSection.sourcedId' => 'lis_course_section_sourcedid','CourseSection.longDescription' => '$COURSE->summary','CourseSection.timeFrame.begin' => null,'CourseSection.timeFrame.end' => null,'ResourceLink.id' => 'resource_link_id','ResourceLink.title' => 'resource_link_title','ResourceLink.description' => 'resource_link_description','User.id' => 'user_id','User.username' => '$USER->username','Person.name.full' => 'lis_person_name_full','Person.name.given' => 'lis_person_name_given','Person.name.family' => 'lis_person_name_family','Person.email.primary' => 'lis_person_contact_email_primary','Person.sourcedId' => 'lis_person_sourcedid','Person.name.middle' => '$USER->middlename','Person.address.street1' => '$USER->address','Person.address.locality' => '$USER->city','Person.address.country' => '$USER->country','Person.address.timezone' => '$USER->timezone','Person.phone.primary' => '$USER->phone1','Person.phone.mobile' => '$USER->phone2','Person.webaddress' => '$USER->url','Membership.role' => 'roles','Result.sourcedId' => 'lis_result_sourcedid','Result.autocreate' => 'lis_outcome_service_url','BasicOutcome.sourcedId' => 'lis_result_sourcedid','BasicOutcome.url' => 'lis_outcome_service_url','Moodle.Person.userGroupIds' => null);return $capabilities;}/*** Initializes an array with the services supported by the LTI module** @return array List of services*/function lti_get_services() {$services = array();$definedservices = core_component::get_plugin_list('ltiservice');foreach ($definedservices as $name => $location) {$classname = "\\ltiservice_{$name}\\local\\service\\{$name}";$services[] = new $classname();}return $services;}/*** Initializes an instance of the named service** @param string $servicename Name of service** @return bool|\mod_lti\local\ltiservice\service_base Service*/function lti_get_service_by_name($servicename) {$service = false;$classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}";if (class_exists($classname)) {$service = new $classname();}return $service;}/*** Finds a service by id** @param \mod_lti\local\ltiservice\service_base[] $services Array of services* @param string $resourceid ID of resource** @return mod_lti\local\ltiservice\service_base Service*/function lti_get_service_by_resource_id($services, $resourceid) {$service = false;foreach ($services as $aservice) {foreach ($aservice->get_resources() as $resource) {if ($resource->get_id() === $resourceid) {$service = $aservice;break 2;}}}return $service;}/*** Initializes an array with the scopes for services supported by the LTI module* and authorized for this particular tool instance.** @param object $type LTI tool type* @param array $typeconfig LTI tool type configuration** @return array List of scopes*/function lti_get_permitted_service_scopes($type, $typeconfig) {$services = lti_get_services();$scopes = array();foreach ($services as $service) {$service->set_type($type);$service->set_typeconfig($typeconfig);$servicescopes = $service->get_permitted_scopes();if (!empty($servicescopes)) {$scopes = array_merge($scopes, $servicescopes);}}return $scopes;}/*** Extracts the named contexts from a tool proxy** @param object $json** @return array Contexts*/function lti_get_contexts($json) {$contexts = array();if (isset($json->{'@context'})) {foreach ($json->{'@context'} as $context) {if (is_object($context)) {$contexts = array_merge(get_object_vars($context), $contexts);}}}return $contexts;}/*** Converts an ID to a fully-qualified ID** @param array $contexts* @param string $id** @return string Fully-qualified ID*/function lti_get_fqid($contexts, $id) {$parts = explode(':', $id, 2);if (count($parts) > 1) {if (array_key_exists($parts[0], $contexts)) {$id = $contexts[$parts[0]] . $parts[1];}}return $id;}/*** Returns the icon for the given tool type** @param stdClass $type The tool type** @return string The url to the tool type's corresponding icon*/function get_tool_type_icon_url(stdClass $type) {global $OUTPUT;$iconurl = $type->secureicon;if (empty($iconurl)) {$iconurl = $type->icon;}if (empty($iconurl)) {$iconurl = $OUTPUT->image_url('monologo', 'lti')->out();}return $iconurl;}/*** Returns the edit url for the given tool type** @param stdClass $type The tool type** @return string The url to edit the tool type*/function get_tool_type_edit_url(stdClass $type) {$url = new moodle_url('/mod/lti/typessettings.php',array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));return $url->out();}/*** Returns the edit url for the given tool proxy.** @param stdClass $proxy The tool proxy** @return string The url to edit the tool type*/function get_tool_proxy_edit_url(stdClass $proxy) {$url = new moodle_url('/mod/lti/registersettings.php',array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure'));return $url->out();}/*** Returns the course url for the given tool type** @param stdClass $type The tool type** @return string The url to the course of the tool type, void if it is a site wide type*/function get_tool_type_course_url(stdClass $type) {if ($type->course != 1) {$url = new moodle_url('/course/view.php', array('id' => $type->course));return $url->out();}return null;}/*** Returns the icon and edit urls for the tool type and the course url if it is a course type.** @param stdClass $type The tool type** @return array The urls of the tool type*/function get_tool_type_urls(stdClass $type) {$courseurl = get_tool_type_course_url($type);$urls = array('icon' => get_tool_type_icon_url($type),'edit' => get_tool_type_edit_url($type),);if ($courseurl) {$urls['course'] = $courseurl;}$url = new moodle_url('/mod/lti/certs.php');$urls['publickeyset'] = $url->out();$url = new moodle_url('/mod/lti/token.php');$urls['accesstoken'] = $url->out();$url = new moodle_url('/mod/lti/auth.php');$urls['authrequest'] = $url->out();return $urls;}/*** Returns the icon and edit urls for the tool proxy.** @param stdClass $proxy The tool proxy** @return array The urls of the tool proxy*/function get_tool_proxy_urls(stdClass $proxy) {global $OUTPUT;$urls = array('icon' => $OUTPUT->image_url('monologo', 'lti')->out(),'edit' => get_tool_proxy_edit_url($proxy),);return $urls;}/*** Returns information on the current state of the tool type** @param stdClass $type The tool type** @return array An array with a text description of the state, and boolean for whether it is in each state:* pending, configured, rejected, unknown*/function get_tool_type_state_info(stdClass $type) {$isconfigured = false;$ispending = false;$isrejected = false;$isunknown = false;switch ($type->state) {case LTI_TOOL_STATE_CONFIGURED:$state = get_string('active', 'mod_lti');$isconfigured = true;break;case LTI_TOOL_STATE_PENDING:$state = get_string('pending', 'mod_lti');$ispending = true;break;case LTI_TOOL_STATE_REJECTED:$state = get_string('rejected', 'mod_lti');$isrejected = true;break;default:$state = get_string('unknownstate', 'mod_lti');$isunknown = true;break;}return array('text' => $state,'pending' => $ispending,'configured' => $isconfigured,'rejected' => $isrejected,'unknown' => $isunknown);}/*** Returns information on the configuration of the tool type** @param stdClass $type The tool type** @return array An array with configuration details*/function get_tool_type_config($type) {global $CFG;$platformid = $CFG->wwwroot;$clientid = $type->clientid;$deploymentid = $type->id;$publickeyseturl = new moodle_url('/mod/lti/certs.php');$publickeyseturl = $publickeyseturl->out();$accesstokenurl = new moodle_url('/mod/lti/token.php');$accesstokenurl = $accesstokenurl->out();$authrequesturl = new moodle_url('/mod/lti/auth.php');$authrequesturl = $authrequesturl->out();return array('platformid' => $platformid,'clientid' => $clientid,'deploymentid' => $deploymentid,'publickeyseturl' => $publickeyseturl,'accesstokenurl' => $accesstokenurl,'authrequesturl' => $authrequesturl);}/*** Returns a summary of each LTI capability this tool type requires in plain language** @param stdClass $type The tool type** @return array An array of text descriptions of each of the capabilities this tool type requires*/function get_tool_type_capability_groups($type) {$capabilities = lti_get_enabled_capabilities($type);$groups = array();$hascourse = false;$hasactivities = false;$hasuseraccount = false;$hasuserpersonal = false;foreach ($capabilities as $capability) {// Bail out early if we've already found all groups.if (count($groups) >= 4) {continue;}if (!$hascourse && preg_match('/^CourseSection/', $capability)) {$hascourse = true;$groups[] = get_string('courseinformation', 'mod_lti');} else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) {$hasactivities = true;$groups[] = get_string('courseactivitiesorresources', 'mod_lti');} else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) {$hasuseraccount = true;$groups[] = get_string('useraccountinformation', 'mod_lti');} else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) {$hasuserpersonal = true;$groups[] = get_string('userpersonalinformation', 'mod_lti');}}return $groups;}/*** Returns the ids of each instance of this tool type** @param stdClass $type The tool type** @return array An array of ids of the instances of this tool type*/function get_tool_type_instance_ids($type) {global $DB;return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id)));}/*** Serialises this tool type** @param stdClass $type The tool type** @return array An array of values representing this type*/function serialise_tool_type(stdClass $type) {global $CFG;$capabilitygroups = get_tool_type_capability_groups($type);$instanceids = get_tool_type_instance_ids($type);// Clean the name. We don't want tags here.$name = clean_param($type->name, PARAM_NOTAGS);if (!empty($type->description)) {// Clean the description. We don't want tags here.$description = clean_param($type->description, PARAM_NOTAGS);} else {$description = get_string('editdescription', 'mod_lti');}return array('id' => $type->id,'name' => $name,'description' => $description,'urls' => get_tool_type_urls($type),'state' => get_tool_type_state_info($type),'platformid' => $CFG->wwwroot,'clientid' => $type->clientid,'deploymentid' => $type->id,'hascapabilitygroups' => !empty($capabilitygroups),'capabilitygroups' => $capabilitygroups,// Course ID of 1 means it's not linked to a course.'courseid' => $type->course == 1 ? 0 : $type->course,'instanceids' => $instanceids,'instancecount' => count($instanceids));}/*** Loads the cartridge information into the tool type, if the launch url is for a cartridge file** @param stdClass $type The tool type object to be filled in* @since Moodle 3.1*/function lti_load_type_if_cartridge($type) {if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) {lti_load_type_from_cartridge($type->lti_toolurl, $type);}}/*** Loads the cartridge information into the new tool, if the launch url is for a cartridge file** @param stdClass $lti The tools config* @since Moodle 3.1*/function lti_load_tool_if_cartridge($lti) {if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) {lti_load_tool_from_cartridge($lti->toolurl, $lti);}}/*** Determines if the given url is for a IMS basic cartridge** @param string $url The url to be checked* @return True if the url is for a cartridge* @since Moodle 3.1*/function lti_is_cartridge($url) {// If it is empty, it's not a cartridge.if (empty($url)) {return false;}// If it has xml at the end of the url, it's a cartridge.if (preg_match('/\.xml$/', $url)) {return true;}// Even if it doesn't have .xml, load the url to check if it's a cartridge..try {$toolinfo = lti_load_cartridge($url,array("launch_url" => "launchurl"));if (!empty($toolinfo['launchurl'])) {return true;}} catch (moodle_exception $e) {return false; // Error loading the xml, so it's not a cartridge.}return false;}/*** Allows you to load settings for an external tool type from an IMS cartridge.** @param string $url The URL to the cartridge* @param stdClass $type The tool type object to be filled in* @throws moodle_exception if the cartridge could not be loaded correctly* @since Moodle 3.1*/function lti_load_type_from_cartridge($url, $type) {$toolinfo = lti_load_cartridge($url,array("title" => "lti_typename","launch_url" => "lti_toolurl","description" => "lti_description","icon" => "lti_icon","secure_icon" => "lti_secureicon"),array("icon_url" => "lti_extension_icon","secure_icon_url" => "lti_extension_secureicon"));// If an activity name exists, unset the cartridge name so we don't override it.if (isset($type->lti_typename)) {unset($toolinfo['lti_typename']);}// Always prefer cartridge core icons first, then, if none are found, look at the extension icons.if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {$toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];}unset($toolinfo['lti_extension_icon']);if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {$toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];}unset($toolinfo['lti_extension_secureicon']);// Ensure Custom icons aren't overridden by cartridge params.if (!empty($type->lti_icon)) {unset($toolinfo['lti_icon']);}if (!empty($type->lti_secureicon)) {unset($toolinfo['lti_secureicon']);}foreach ($toolinfo as $property => $value) {$type->$property = $value;}}/*** Allows you to load in the configuration for an external tool from an IMS cartridge.** @param string $url The URL to the cartridge* @param stdClass $lti LTI object* @throws moodle_exception if the cartridge could not be loaded correctly* @since Moodle 3.1*/function lti_load_tool_from_cartridge($url, $lti) {$toolinfo = lti_load_cartridge($url,array("title" => "name","launch_url" => "toolurl","secure_launch_url" => "securetoolurl","description" => "intro","icon" => "icon","secure_icon" => "secureicon"),array("icon_url" => "extension_icon","secure_icon_url" => "extension_secureicon"));// If an activity name exists, unset the cartridge name so we don't override it.if (isset($lti->name)) {unset($toolinfo['name']);}// Always prefer cartridge core icons first, then, if none are found, look at the extension icons.if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {$toolinfo['icon'] = $toolinfo['extension_icon'];}unset($toolinfo['extension_icon']);if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {$toolinfo['secureicon'] = $toolinfo['extension_secureicon'];}unset($toolinfo['extension_secureicon']);foreach ($toolinfo as $property => $value) {$lti->$property = $value;}}/*** Search for a tag within an XML DOMDocument** @param string $url The url of the cartridge to be loaded* @param array $map The map of tags to keys in the return array* @param array $propertiesmap The map of properties to keys in the return array* @return array An associative array with the given keys and their values from the cartridge* @throws moodle_exception if the cartridge could not be loaded correctly* @since Moodle 3.1*/function lti_load_cartridge($url, $map, $propertiesmap = array()) {global $CFG;require_once($CFG->libdir. "/filelib.php");$curl = new curl();$response = $curl->get($url);// Got a completely empty response (real or error), cannot process this with// DOMDocument::loadXML() because it errors with ValueError. So let's throw// the moodle_exception before waiting to examine the errors later.if (trim($response) === '') {throw new moodle_exception('errorreadingfile', '', '', $url);}// TODO MDL-46023 Replace this code with a call to the new library.$origerrors = libxml_use_internal_errors(true);libxml_clear_errors();$document = new DOMDocument();@$document->loadXML($response, LIBXML_NONET);$cartridge = new DomXpath($document);$errors = libxml_get_errors();libxml_clear_errors();libxml_use_internal_errors($origerrors);if (count($errors) > 0) {$message = 'Failed to load cartridge.';foreach ($errors as $error) {$message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line;}throw new moodle_exception('errorreadingfile', '', '', $url, $message);}$toolinfo = array();foreach ($map as $tag => $key) {$value = get_tag($tag, $cartridge);if ($value) {$toolinfo[$key] = $value;}}if (!empty($propertiesmap)) {foreach ($propertiesmap as $property => $key) {$value = get_tag("property", $cartridge, $property);if ($value) {$toolinfo[$key] = $value;}}}return $toolinfo;}/*** Search for a tag within an XML DOMDocument** @param stdClass $tagname The name of the tag to search for* @param XPath $xpath The XML to find the tag in* @param XPath $attribute The attribute to search for (if we should search for a child node with the given* value for the name attribute* @since Moodle 3.1*/function get_tag($tagname, $xpath, $attribute = null) {if ($attribute) {$result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]');} else {$result = $xpath->query('//*[local-name() = \'' . $tagname . '\']');}if ($result->length > 0) {return $result->item(0)->nodeValue;}return null;}/*** Create a new access token.** @param int $typeid Tool type ID* @param string[] $scopes Scopes permitted for new token** @return stdClass Access token*/function lti_new_access_token($typeid, $scopes) {global $DB;// Make sure the token doesn't exist (even if it should be almost impossible with the random generation).$numtries = 0;do {$numtries ++;$generatedtoken = md5(uniqid(rand(), 1));if ($numtries > 5) {throw new moodle_exception('Failed to generate LTI access token');}} while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken)));$newtoken = new stdClass();$newtoken->typeid = $typeid;$newtoken->scope = json_encode(array_values($scopes));$newtoken->token = $generatedtoken;$newtoken->timecreated = time();$newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE;$newtoken->lastaccess = null;$DB->insert_record('lti_access_tokens', $newtoken);return $newtoken;}/*** Wrapper for function libxml_disable_entity_loader() deprecated in PHP 8** Method was deprecated in PHP 8 and it shows deprecation message. However it is still* required in the previous versions on PHP. While Moodle supports both PHP 7 and 8 we need to keep it.* @see https://php.watch/versions/8.0/libxml_disable_entity_loader-deprecation** @param bool $value* @return bool** @deprecated since Moodle 4.3*/function lti_libxml_disable_entity_loader(bool $value): bool {debugging(__FUNCTION__ . '() is deprecated, please do not use it any more', DEBUG_DEVELOPER);return true;}