diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php
index e84c049b5a8..4eaa74220b8 100644
--- a/lib/classes/plugin_manager.php
+++ b/lib/classes/plugin_manager.php
@@ -1741,6 +1741,10 @@ class core_plugin_manager {
'gregorian'
),
+ 'customfield' => array(
+ 'checkbox', 'date', 'select', 'text', 'textarea'
+ ),
+
'coursereport' => array(
// Deprecated!
),
diff --git a/mod/lti/OAuthBody.php b/mod/lti/OAuthBody.php
index 761b01c1618..147b695bb81 100644
--- a/mod/lti/OAuthBody.php
+++ b/mod/lti/OAuthBody.php
@@ -44,7 +44,8 @@ require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
* @param int $typeid LTI type ID.
* @param string[] $scopes Array of scopes which give permission for the current request.
*
- * @return string|int|boolean The OAuth consumer key, the LTI type ID for the validated bearer token, otherwise false.
+ * @return string|int|boolean The OAuth consumer key, the LTI type ID for the validated bearer token,
+ true for requests not requiring a scope, otherwise false.
*/
function get_oauth_key_from_headers($typeid = null, $scopes = null) {
global $DB;
diff --git a/mod/lti/auth.php b/mod/lti/auth.php
index 6f4da8a1db7..68c5ccdeb69 100644
--- a/mod/lti/auth.php
+++ b/mod/lti/auth.php
@@ -15,7 +15,7 @@
// along with Moodle. If not, see .
/**
- * This file returns an array of available public keys
+ * This file responds to a login authentication request
*
* @package mod_lti
* @copyright 2019 Stephen Vickers
@@ -37,7 +37,7 @@ $nonce = optional_param('nonce', '', PARAM_TEXT);
$prompt = optional_param('prompt', '', PARAM_TEXT);
$ok = !empty($scope) && !empty($responsetype) && !empty($clientid) &&
- !empty($redirecturi) && !empty($loginhint) && !empty($scope) &&
+ !empty($redirecturi) && !empty($loginhint) &&
!empty($nonce) && !empty($SESSION->lti_message_hint);
if (!$ok) {
@@ -84,7 +84,9 @@ if ($ok) {
$desc = 'Invalid response_mode';
}
} else {
- $responsemode = 'query';
+ $ok = false;
+ $error = 'invalid_request';
+ $desc = 'Missing response_mode';
}
}
if ($ok && !empty($prompt) && ($prompt !== 'none')) {
@@ -132,24 +134,19 @@ if (isset($state)) {
$params['state'] = $state;
}
unset($SESSION->lti_message_hint);
-if ($responsemode !== 'query') {
- $r = '
\n";
- $r .= "\n";
- echo $r;
-} else {
- $url = new \moodle_url(redirecturi, $params);
- redirect($url->out(false));
}
+$r .= "\n";
+$r .= "\n";
+echo $r;
diff --git a/mod/lti/backup/moodle2/backup_lti_stepslib.php b/mod/lti/backup/moodle2/backup_lti_stepslib.php
index 7b4b45760c4..eae88a71bf4 100644
--- a/mod/lti/backup/moodle2/backup_lti_stepslib.php
+++ b/mod/lti/backup/moodle2/backup_lti_stepslib.php
@@ -99,6 +99,8 @@ class backup_lti_activity_structure_step extends backup_activity_structure_step
'state',
'course',
'coursevisible',
+ 'ltiversion',
+ 'clientid',
'toolproxyid',
'enabledcapability',
'parameter',
diff --git a/mod/lti/classes/local/ltiservice/service_base.php b/mod/lti/classes/local/ltiservice/service_base.php
index c896c6a6a20..4e30db0b16e 100644
--- a/mod/lti/classes/local/ltiservice/service_base.php
+++ b/mod/lti/classes/local/ltiservice/service_base.php
@@ -375,11 +375,7 @@ abstract class service_base {
$key = $this->toolproxy->guid;
$secret = $this->toolproxy->secret;
} else {
- if ($this->type->ltiversion === LTI_VERSION_1P3) {
- $key = $this->type->clientid; // Should not be needed as access tokens are used.
- } else {
- $key = $this->typeconfig['resourcekey'];
- }
+ $key = $this->typeconfig['resourcekey'];
$secret = $this->typeconfig['password'];
}
if (!$this->is_unsigned() && ($key == $consumerkey)) {
diff --git a/mod/lti/contentitem.php b/mod/lti/contentitem.php
index 0c954949782..2528cba1c65 100644
--- a/mod/lti/contentitem.php
+++ b/mod/lti/contentitem.php
@@ -35,7 +35,7 @@ $text = optional_param('text', '', PARAM_RAW);
$config = lti_get_type_type_config($id);
if ($config->lti_ltiversion === LTI_VERSION_1P3) {
if (!isset($SESSION->lti_initiatelogin_status)) {
- echo lti_initiatelogin($courseid, 0, null, $config, 'ContentItemSelectionRequest', $title, $text);
+ echo lti_initiate_login($courseid, 0, null, $config, 'ContentItemSelectionRequest', $title, $text);
exit;
} else {
unset($SESSION->lti_initiatelogin_status);
diff --git a/mod/lti/contentitem_return.php b/mod/lti/contentitem_return.php
index a6a1b7dfc00..cc29ae7049a 100644
--- a/mod/lti/contentitem_return.php
+++ b/mod/lti/contentitem_return.php
@@ -33,12 +33,12 @@ $jwt = optional_param('JWT', '', PARAM_RAW);
if (!empty($jwt)) {
list($tool, $params) = lti_convert_from_jwt($id, $jwt);
- $consumerkey = isset($params['oauth_consumer_key']) ? $params['oauth_consumer_key'] : '';
- $messagetype = isset($params['lti_message_type']) ? $params['lti_message_type'] : '';
- $version = isset($params['lti_version']) ? $params['lti_version'] : '';
- $items = isset($params['content_items']) ? $params['content_items'] : '';
- $errormsg = isset($params['lti_errormsg']) ? $params['lti_errormsg'] : '';
- $msg = isset($params['lti_msg']) ? $params['lti_msg'] : '';
+ $consumerkey = $params['oauth_consumer_key'] ?? '';
+ $messagetype = $params['lti_message_type'] ?? '';
+ $version = $params['lti_version'] ?? '';
+ $items = $params['content_items'] ?? '';
+ $errormsg = $params['lti_errormsg'] ?? '';
+ $msg = $params['lti_msg'] ?? '';
} else {
$consumerkey = required_param('oauth_consumer_key', PARAM_RAW);
$messagetype = required_param('lti_message_type', PARAM_TEXT);
@@ -60,7 +60,7 @@ $redirecturl = null;
$returndata = null;
if (empty($errormsg) && !empty($items)) {
try {
- $returndata = lti_tool_configuration_from_content_item($tool, $messagetype, $version, $consumerkey, $items);
+ $returndata = lti_tool_configuration_from_content_item($id, $messagetype, $version, $consumerkey, $items);
} catch (moodle_exception $e) {
$errormsg = $e->getMessage();
}
diff --git a/mod/lti/launch.php b/mod/lti/launch.php
index 20b04b455e7..6da25d532f5 100644
--- a/mod/lti/launch.php
+++ b/mod/lti/launch.php
@@ -61,7 +61,7 @@ if ($typeid) {
$config = lti_get_type_type_config($typeid);
if ($config->lti_ltiversion === LTI_VERSION_1P3) {
if (!isset($SESSION->lti_initiatelogin_status)) {
- echo lti_initiatelogin($cm->course, $id, $lti, $config);
+ echo lti_initiate_login($cm->course, $id, $lti, $config);
exit;
} else {
unset($SESSION->lti_initiatelogin_status);
diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php
index 1c13c275b7d..1ea11a9e878 100644
--- a/mod/lti/locallib.php
+++ b/mod/lti/locallib.php
@@ -496,7 +496,7 @@ function lti_get_jwt_claim_mapping() {
* Return the launch data required for opening the external tool.
*
* @param stdClass $instance the external tool activity settings
- * @param string $nonce
+ * @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
*/
@@ -666,7 +666,11 @@ function lti_get_launch_data($instance, $nonce = '') {
}
if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) {
- $parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret, $typeid, $nonce);
+ 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();
@@ -926,6 +930,8 @@ function lti_build_request_lti2($tool, $params) {
* @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) {
@@ -1057,7 +1063,7 @@ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $cus
function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
$presentationtargets = [], $autocreate = false, $multiple = false,
$unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {
- global $PAGE, $USER;
+ global $USER;
$tool = lti_get_type($id);
// Validate parameters.
@@ -1157,7 +1163,7 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
$services = lti_get_services();
foreach ($services as $service) {
$serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
- $PAGE->course->id, $USER->id , $id);
+ $course->id, $USER->id , $id);
foreach ($serviceparameters as $paramkey => $paramvalue) {
$requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
$islti2);
@@ -1207,7 +1213,11 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
$requestparams['content_item_return_url'] = $returnurl->out(false);
$requestparams['title'] = $title;
$requestparams['text'] = $text;
- $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret, $id, $nonce);
+ 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.
@@ -1259,12 +1269,10 @@ function lti_verify_oauth_signature($typeid, $consumerkey) {
$typeconfig = lti_get_type_config($typeid);
if (isset($tool->toolproxyid)) {
- $islti2 = true;
$toolproxy = lti_get_tool_proxy($tool->toolproxyid);
$key = $toolproxy->guid;
$secret = $toolproxy->secret;
} else {
- $islti2 = false;
$toolproxy = null;
if (!empty($typeconfig['resourcekey'])) {
$key = $typeconfig['resourcekey'];
@@ -1306,6 +1314,11 @@ function lti_verify_oauth_signature($typeid, $consumerkey) {
*
* @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);
@@ -1313,22 +1326,15 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
if (!$tool) {
throw new moodle_exception('errortooltypenotfound', 'mod_lti');
}
- $typeconfig = lti_get_type_config($typeid);
-
if (isset($tool->toolproxyid)) {
throw new moodle_exception('JWT security not supported with LTI 2');
- } else {
- if (!empty($tool->clientid)) {
- $key = $tool->clientid;
- } else {
- $key = '';
- }
- if (!empty($typeconfig['publickey'])) {
- $publickey = $typeconfig['publickey'];
- } else {
- $publickey = '';
- }
}
+
+ $typeconfig = lti_get_type_config($typeid);
+
+ $key = $tool->clientid ?? '';
+ $publickey = $typeconfig['publickey'] ?? '';
+
if ($consumerkey !== $key) {
throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
}
@@ -1336,11 +1342,7 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
throw new moodle_exception('No public key configured');
}
- try {
- $jwt = JWT::decode($jwtparam, $publickey, array('RS256'));
- } catch (Exception $e) {
- throw new moodle_exception($e->getMessage());
- }
+ JWT::decode($jwtparam, $publickey, array('RS256'));
return $tool;
}
@@ -1349,7 +1351,7 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
* 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 stdClass $tool The tool type.
+ * @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.
@@ -1358,7 +1360,12 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
* @throws moodle_exception
* @throws lti\OAuthException
*/
-function lti_tool_configuration_from_content_item($tool, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
+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') {
@@ -1473,6 +1480,7 @@ function lti_tool_configuration_from_content_item($tool, $messagetype, $ltiversi
* 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();
@@ -2984,24 +2992,39 @@ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $insta
* @param string $oauthconsumersecret
* @return array|null
*/
-function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret, $typeid = 0, $nonce = '') {
+function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
$parms = $oldparms;
$testtoken = '';
- if ($parms['lti_version'] !== LTI_VERSION_1P3) {
- // 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);
+ // 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();
- } else {
- $newparms = array();
- $newparms['id_token'] = lti_convert_to_jwt($parms, $endpoint, $oauthconsumerkey, $typeid, $nonce);
- }
+ $newparms = $accreq->get_parameters();
+
+ return $newparms;
+}
+
+/**
+ * Signs the petition to launch the external tool using JWT
+ *
+ * @param array $oldparms 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($oldparms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {
+
+ $parms = $oldparms;
+
+ $newparms = array();
+ $newparms['id_token'] = lti_convert_to_jwt($parms, $endpoint, $oauthconsumerkey, $typeid, $nonce);
return $newparms;
}
@@ -3098,11 +3121,12 @@ function lti_convert_to_jwt($parms, $endpoint, $oauthconsumerkey, $typeid, $nonc
* @param int $typeid
* @param string $jwtparam JWT parameter
*
- * @return stdClass Tool type
+ * @return array containing tool type and the array of parameters
* @throws moodle_exception
*/
function lti_convert_from_jwt($typeid, $jwtparam) {
+ $params = array();
$parts = explode('.', $jwtparam);
$ok = (count($parts) === 3);
if ($ok) {
@@ -3112,75 +3136,59 @@ function lti_convert_from_jwt($typeid, $jwtparam) {
}
if ($ok) {
$tool = lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);
- }
- $params = array();
- $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)) {
- $ok = false;
- $error = "'{$claim}' should be an array";
- } else if (array_key_exists($mapping['claim'],
- $group)) {
- $value = $group[$mapping['claim']];
- }
+ $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']}";
}
- if (!empty($value) && $mapping['isarray']) {
- if (!is_array($value)) {
- $ok = false;
- $error = "'{$value}' should be an array";
- } else if (is_array($value[0])) {
- $value = json_encode($value);
+ $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 {
- $value = implode(',', $value);
+ $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;
}
}
- if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {
- $params[$key] = $value;
- }
- }
- }
- if ($ok) {
- $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';
- if (isset($claims[$claim])) {
- $custom = $claims[$claim];
- if (!is_array($custom)) {
- $ok = false;
- $error = "'{$custom}' should be an array";
- } else {
- foreach ($custom as $key => $value) {
- $params["custom_{$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;
+ }
}
}
- }
- }
- if ($ok) {
- $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';
- if (isset($claims[$claim])) {
- $ext = $claims[$claim];
- if (!is_array($ext)) {
- $ok = false;
- $error = "'{$ext}' should be an array";
- } else {
- foreach ($ext as $key => $value) {
- $params["ext_{$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;
+ }
}
}
}
@@ -3273,8 +3281,8 @@ function lti_post_launch_html($newparms, $endpoint, $debug=false) {
* @param string $text Description of content item
* @return string
*/
-function lti_initiatelogin($courseid, $id, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '', $text = '') {
- global $SESSION, $USER, $COURSE;
+function lti_initiate_login($courseid, $id, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '', $text = '') {
+ global $SESSION, $USER;
if (!empty($instance)) {
$endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;
@@ -3287,7 +3295,7 @@ function lti_initiatelogin($courseid, $id, $instance, $config, $messagetype = 'b
$endpoint = trim($endpoint);
// If SSL is forced make sure https is on the normal launch URL.
- if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) {
+ if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {
$endpoint = lti_ensure_url_is_https($endpoint);
} else if (!strstr($endpoint, '://')) {
$endpoint = 'http://' . $endpoint;
diff --git a/mod/lti/view.php b/mod/lti/view.php
index e186f6cb039..06bc4521028 100644
--- a/mod/lti/view.php
+++ b/mod/lti/view.php
@@ -141,7 +141,7 @@ if (($launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW) &&
} else {
$content = '';
if ($config->lti_ltiversion === LTI_VERSION_1P3) {
- $content = lti_initiatelogin($cm->course, $id, $lti, $config);
+ $content = lti_initiate_login($cm->course, $id, $lti, $config);
}
// Request the launch content with an iframe tag.