first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
define("enrol_lti/content_select",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{document.addEventListener("change",(e=>{if(e.target.matches("input[type='checkbox'][name^='modules']")){const value=e.target.value,gradecheckbox=document.querySelector("input[type='checkbox'][name^='grades'][value='"+value+"']");gradecheckbox&&(gradecheckbox.checked=e.target.checked)}if(e.target.matches("input[type='checkbox'][name^='grades']")){const value=e.target.value,modcheckbox=document.querySelector("input[type='checkbox'][name^='modules'][value='"+value+"']");e.target.checked&&(modcheckbox.checked=!0)}}))}}));
//# sourceMappingURL=content_select.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"content_select.min.js","sources":["../src/content_select.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n/**\n * Module providing checkbox autoselection behaviour to the table on the select content deep linking view, launch_deeplink.php.\n *\n * @module enrol_lti/content_select\n * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n// Register the checkbox change events allowing the automatic selection/deselection of the\n// 'add to gradebook' and 'add to course' checkboxes when selecting an activity/resource.\nconst registerEventHandlers = () => {\n document.addEventListener('change', e => {\n if (e.target.matches(\"input[type='checkbox'][name^='modules']\")) {\n const value = e.target.value;\n const gradecheckbox = document.querySelector(\"input[type='checkbox'][name^='grades'][value='\" + value + \"']\");\n if (gradecheckbox) {\n gradecheckbox.checked = e.target.checked;\n }\n }\n\n if (e.target.matches(\"input[type='checkbox'][name^='grades']\")) {\n const value = e.target.value;\n const modcheckbox = document.querySelector(\"input[type='checkbox'][name^='modules'][value='\" + value + \"']\");\n if (e.target.checked) {\n modcheckbox.checked = true;\n }\n }\n });\n};\n\nexport const init = () => {\n registerEventHandlers();\n};\n"],"names":["document","addEventListener","e","target","matches","value","gradecheckbox","querySelector","checked","modcheckbox"],"mappings":"6JA2CoB,KAnBhBA,SAASC,iBAAiB,UAAUC,OAC5BA,EAAEC,OAAOC,QAAQ,2CAA4C,OACvDC,MAAQH,EAAEC,OAAOE,MACjBC,cAAgBN,SAASO,cAAc,iDAAmDF,MAAQ,MACpGC,gBACAA,cAAcE,QAAUN,EAAEC,OAAOK,YAIrCN,EAAEC,OAAOC,QAAQ,0CAA2C,OACtDC,MAAQH,EAAEC,OAAOE,MACjBI,YAAcT,SAASO,cAAc,kDAAoDF,MAAQ,MACnGH,EAAEC,OAAOK,UACTC,YAAYD,SAAU"}
+11
View File
@@ -0,0 +1,11 @@
define("enrol_lti/tool_endpoints",["exports","core/copy_to_clipboard"],(function(_exports,_copy_to_clipboard){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* Module supporting the dynamic and manual registration URLs in the tool registration admin setting.
*
* @module enrol_lti/tool_endpoints
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const SELECTORS_URL_VALUE='[id^="lti_tool_endpoint_url_"]',focusURLHandler=event=>{const triggerElement=event.target.closest(SELECTORS_URL_VALUE);null!==triggerElement&&(event.preventDefault(),triggerElement.select())};_exports.init=()=>{document.addEventListener("focusin",focusURLHandler)}}));
//# sourceMappingURL=tool_endpoints.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"tool_endpoints.min.js","sources":["../src/tool_endpoints.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n/**\n * Module supporting the dynamic and manual registration URLs in the tool registration admin setting.\n *\n * @module enrol_lti/tool_endpoints\n * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport 'core/copy_to_clipboard';\n\n/**\n * DOM Selectors.\n * @type {{URL_VALUE: string}}\n */\nconst SELECTORS = {\n URL_VALUE: '[id^=\"lti_tool_endpoint_url_\"]',\n};\n\n/**\n * Focus handler for the registration URL field, enabling auto select of text on click.\n *\n * @param {Event} event a click event.\n */\nconst focusURLHandler = (event) => {\n const triggerElement = event.target.closest(SELECTORS.URL_VALUE);\n if (triggerElement === null) {\n return;\n }\n event.preventDefault();\n\n triggerElement.select();\n};\n\n/**\n * Initialise the tool registration page, attaching handlers, etc.\n */\nexport const init = () => {\n // Event delegation supporting the select on focus behaviour (with text selection permitted on subsequent clicks).\n document.addEventListener('focusin', focusURLHandler);\n};\n"],"names":["SELECTORS","focusURLHandler","event","triggerElement","target","closest","preventDefault","select","document","addEventListener"],"mappings":";;;;;;;;MA2BMA,oBACS,iCAQTC,gBAAmBC,cACfC,eAAiBD,MAAME,OAAOC,QAAQL,qBACrB,OAAnBG,iBAGJD,MAAMI,iBAENH,eAAeI,yBAMC,KAEhBC,SAASC,iBAAiB,UAAWR"}
+46
View File
@@ -0,0 +1,46 @@
// 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/>.
/**
* Module providing checkbox autoselection behaviour to the table on the select content deep linking view, launch_deeplink.php.
*
* @module enrol_lti/content_select
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// Register the checkbox change events allowing the automatic selection/deselection of the
// 'add to gradebook' and 'add to course' checkboxes when selecting an activity/resource.
const registerEventHandlers = () => {
document.addEventListener('change', e => {
if (e.target.matches("input[type='checkbox'][name^='modules']")) {
const value = e.target.value;
const gradecheckbox = document.querySelector("input[type='checkbox'][name^='grades'][value='" + value + "']");
if (gradecheckbox) {
gradecheckbox.checked = e.target.checked;
}
}
if (e.target.matches("input[type='checkbox'][name^='grades']")) {
const value = e.target.value;
const modcheckbox = document.querySelector("input[type='checkbox'][name^='modules'][value='" + value + "']");
if (e.target.checked) {
modcheckbox.checked = true;
}
}
});
};
export const init = () => {
registerEventHandlers();
};
+53
View File
@@ -0,0 +1,53 @@
// 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/>.
/**
* Module supporting the dynamic and manual registration URLs in the tool registration admin setting.
*
* @module enrol_lti/tool_endpoints
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import 'core/copy_to_clipboard';
/**
* DOM Selectors.
* @type {{URL_VALUE: string}}
*/
const SELECTORS = {
URL_VALUE: '[id^="lti_tool_endpoint_url_"]',
};
/**
* Focus handler for the registration URL field, enabling auto select of text on click.
*
* @param {Event} event a click event.
*/
const focusURLHandler = (event) => {
const triggerElement = event.target.closest(SELECTORS.URL_VALUE);
if (triggerElement === null) {
return;
}
event.preventDefault();
triggerElement.select();
};
/**
* Initialise the tool registration page, attaching handlers, etc.
*/
export const init = () => {
// Event delegation supporting the select on focus behaviour (with text selection permitted on subsequent clicks).
document.addEventListener('focusin', focusURLHandler);
};
@@ -0,0 +1,71 @@
<?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/>.
/**
* Defines the backup_enrol_lti_plugin class.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Define all the backup steps.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_enrol_lti_plugin extends backup_enrol_plugin {
/**
* Defines the other LTI enrolment structures to append.
*
* @return backup_plugin_element
*/
public function define_enrol_plugin_structure() {
// Get the parent we will be adding these elements to.
$plugin = $this->get_plugin_element();
// Define our elements.
$tool = new backup_nested_element('tool', array('id'), array(
'enrolid', 'contextid', 'institution', 'lang', 'timezone', 'maxenrolled', 'maildisplay', 'city',
'country', 'gradesync', 'gradesynccompletion', 'membersync', 'membersyncmode', 'roleinstructor',
'rolelearner', 'secret', 'ltiversion', 'timecreated', 'timemodified'));
$users = new backup_nested_element('users');
$user = new backup_nested_element('user', array('id'), array(
'userid', 'toolid', 'serviceurl', 'sourceid', 'consumerkey', 'consumersecret', 'membershipurl',
'membershipsid'));
// Build elements hierarchy.
$plugin->add_child($tool);
$tool->add_child($users);
$users->add_child($user);
// Set sources to populate the data.
$tool->set_source_table('enrol_lti_tools',
array('enrolid' => backup::VAR_PARENTID));
// Users are only added only if users included.
if ($this->task->get_setting_value('users')) {
$user->set_source_table('enrol_lti_users', array('toolid' => backup::VAR_PARENTID));
}
}
}
@@ -0,0 +1,125 @@
<?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/>.
/**
* Defines the restore_enrol_lti_plugin class.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Define all the restore steps.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_enrol_lti_plugin extends restore_enrol_plugin {
/**
* @var array $tools Stores the IDs of the newly created tools.
*/
protected $tools = array();
/**
* Declares the enrol LTI XML paths attached to the enrol element
*
* @return array of {@link restore_path_element}
*/
protected function define_enrol_plugin_structure() {
$paths = array();
$paths[] = new restore_path_element('enrol_lti_tool', $this->connectionpoint->get_path() . '/tool');
$paths[] = new restore_path_element('enrol_lti_users', $this->connectionpoint->get_path() . '/tool/users/user');
return $paths;
}
/**
* Processes LTI tools element data
*
* @param array|stdClass $data
*/
public function process_enrol_lti_tool($data) {
global $DB;
$data = (object) $data;
// Store the old id.
$oldid = $data->id;
// Change the values before we insert it.
$data->timecreated = time();
$data->timemodified = $data->timecreated;
// Set the correct legacy ltiversion when restoring old tools.
if (empty($data->ltiversion)) {
$data->ltiversion = 'LTI-1p0/LTI-2p0';
}
// Generate a new uuid for LTI Advantage restores.
if ($data->ltiversion == 'LTI-1p3') {
$data->uuid = \core\uuid::generate();
}
// Now we can insert the new record.
$data->id = $DB->insert_record('enrol_lti_tools', $data);
// Add the array of tools we need to process later.
$this->tools[$data->id] = $data;
// Set up the mapping.
$this->set_mapping('enrol_lti_tool', $oldid, $data->id);
}
/**
* Processes LTI users element data
*
* @param array|stdClass $data The data to insert as a comment
*/
public function process_enrol_lti_users($data) {
global $DB;
$data = (object) $data;
$data->userid = $this->get_mappingid('user', $data->userid);
$data->toolid = $this->get_mappingid('enrol_lti_tool', $data->toolid);
$data->timecreated = time();
$DB->insert_record('enrol_lti_users', $data);
}
/**
* This function is executed after all the tasks in the plan have been finished.
* This must be done here because the activities have not been restored yet.
*/
public function after_restore_enrol() {
global $DB;
// Need to go through and change the values.
foreach ($this->tools as $tool) {
$updatetool = new stdClass();
$updatetool->id = $tool->id;
$updatetool->enrolid = $this->get_mappingid('enrol', $tool->enrolid);
$updatetool->contextid = $this->get_mappingid('context', $tool->contextid);
$DB->update_record('enrol_lti_tools', $updatetool);
}
}
}
+61
View File
@@ -0,0 +1,61 @@
<?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/>.
/**
* Generates an XML IMS Cartridge with the details for the given tool
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot . '/lib/weblib.php');
$toolid = null;
$token = null;
$filearguments = get_file_argument();
$arguments = explode('/', trim($filearguments, '/'));
if (count($arguments) >= 2) { // Can put cartridge.xml at the end, or anything really.
list($toolid, $token) = $arguments;
}
$toolid = optional_param('id', $toolid, PARAM_INT);
$token = optional_param('token', $token, PARAM_ALPHANUM);
// Only show the cartridge if the token parameter is correct.
// If we do not compare with a shared secret, someone could very easily
// guess an id for the enrolment.
if (!\enrol_lti\helper::verify_cartridge_token($toolid, $token)) {
throw new \moodle_exception('incorrecttoken', 'enrol_lti');
}
$tool = \enrol_lti\helper::get_lti_tool($toolid);
if (!is_enabled_auth('lti')) {
throw new \moodle_exception('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti'));
} else if (!enrol_is_enabled('lti')) {
throw new \moodle_exception('enrolisdisabled', 'enrol_lti');
} else if ($tool->status != ENROL_INSTANCE_ENABLED) {
throw new \moodle_exception('enrolisdisabled', 'enrol_lti');
} else {
header('Content-Type: text/xml; charset=utf-8');
echo \enrol_lti\helper::create_cartridge($toolid);
}
File diff suppressed because it is too large Load Diff
+665
View File
@@ -0,0 +1,665 @@
<?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/>.
/**
* LTI enrolment plugin helper.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_lti;
defined('MOODLE_INTERNAL') || die();
/**
* LTI enrolment plugin helper class.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/*
* The value used when we want to enrol new members and unenrol old ones.
*/
const MEMBER_SYNC_ENROL_AND_UNENROL = 1;
/*
* The value used when we want to enrol new members only.
*/
const MEMBER_SYNC_ENROL_NEW = 2;
/*
* The value used when we want to unenrol missing users.
*/
const MEMBER_SYNC_UNENROL_MISSING = 3;
/**
* Code for when an enrolment was successful.
*/
const ENROLMENT_SUCCESSFUL = true;
/**
* Error code for enrolment when max enrolled reached.
*/
const ENROLMENT_MAX_ENROLLED = 'maxenrolledreached';
/**
* Error code for enrolment has not started.
*/
const ENROLMENT_NOT_STARTED = 'enrolmentnotstarted';
/**
* Error code for enrolment when enrolment has finished.
*/
const ENROLMENT_FINISHED = 'enrolmentfinished';
/**
* Error code for when an image file fails to upload.
*/
const PROFILE_IMAGE_UPDATE_SUCCESSFUL = true;
/**
* Error code for when an image file fails to upload.
*/
const PROFILE_IMAGE_UPDATE_FAILED = 'profileimagefailed';
/**
* Creates a unique username.
*
* @param string $consumerkey Consumer key
* @param string $ltiuserid External tool user id
* @return string The new username
*/
public static function create_username($consumerkey, $ltiuserid) {
if (!empty($ltiuserid) && !empty($consumerkey)) {
$userkey = $consumerkey . ':' . $ltiuserid;
} else {
$userkey = false;
}
return 'enrol_lti' . sha1($consumerkey . '::' . $userkey);
}
/**
* Adds default values for the user object based on the tool provided.
*
* @param \stdClass $tool
* @param \stdClass $user
* @return \stdClass The $user class with added default values
*/
public static function assign_user_tool_data($tool, $user) {
global $CFG;
$user->city = (!empty($tool->city)) ? $tool->city : "";
$user->country = (!empty($tool->country)) ? $tool->country : "";
$user->institution = (!empty($tool->institution)) ? $tool->institution : "";
$user->timezone = (!empty($tool->timezone)) ? $tool->timezone : "";
if (isset($tool->maildisplay)) {
$user->maildisplay = $tool->maildisplay;
} else if (isset($CFG->defaultpreference_maildisplay)) {
$user->maildisplay = $CFG->defaultpreference_maildisplay;
} else {
$user->maildisplay = 2;
}
$user->mnethostid = $CFG->mnet_localhost_id;
$user->confirmed = 1;
$user->lang = $tool->lang;
return $user;
}
/**
* Compares two users.
*
* @param \stdClass $newuser The new user
* @param \stdClass $olduser The old user
* @return bool True if both users are the same
*/
public static function user_match($newuser, $olduser) {
if ($newuser->firstname != $olduser->firstname) {
return false;
}
if ($newuser->lastname != $olduser->lastname) {
return false;
}
if ($newuser->email != $olduser->email) {
return false;
}
if ($newuser->city != $olduser->city) {
return false;
}
if ($newuser->country != $olduser->country) {
return false;
}
if ($newuser->institution != $olduser->institution) {
return false;
}
if ($newuser->timezone != $olduser->timezone) {
return false;
}
if ($newuser->maildisplay != $olduser->maildisplay) {
return false;
}
if ($newuser->mnethostid != $olduser->mnethostid) {
return false;
}
if ($newuser->confirmed != $olduser->confirmed) {
return false;
}
if ($newuser->lang != $olduser->lang) {
return false;
}
return true;
}
/**
* Updates the users profile image.
*
* @param int $userid the id of the user
* @param string $url the url of the image
* @return bool|string true if successful, else a string explaining why it failed
*/
public static function update_user_profile_image($userid, $url) {
global $CFG, $DB;
require_once($CFG->libdir . '/filelib.php');
require_once($CFG->libdir . '/gdlib.php');
$fs = get_file_storage();
$context = \context_user::instance($userid, MUST_EXIST);
$fs->delete_area_files($context->id, 'user', 'newicon');
$filerecord = array(
'contextid' => $context->id,
'component' => 'user',
'filearea' => 'newicon',
'itemid' => 0,
'filepath' => '/'
);
$urlparams = array(
'calctimeout' => false,
'timeout' => 5,
'skipcertverify' => true,
'connecttimeout' => 5
);
try {
$fs->create_file_from_url($filerecord, $url, $urlparams);
} catch (\file_exception $e) {
return get_string($e->errorcode, $e->module, $e->a);
}
$iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false);
// There should only be one.
$iconfile = reset($iconfile);
// Something went wrong while creating temp file - remove the uploaded file.
if (!$iconfile = $iconfile->copy_content_to_temp()) {
$fs->delete_area_files($context->id, 'user', 'newicon');
return self::PROFILE_IMAGE_UPDATE_FAILED;
}
// Copy file to temporary location and the send it for processing icon.
$newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile);
// Delete temporary file.
@unlink($iconfile);
// Remove uploaded file.
$fs->delete_area_files($context->id, 'user', 'newicon');
// Set the user's picture.
$DB->set_field('user', 'picture', $newpicture, array('id' => $userid));
return self::PROFILE_IMAGE_UPDATE_SUCCESSFUL;
}
/**
* Enrol a user in a course.
*
* @param \stdclass $tool The tool object (retrieved using self::get_lti_tool() or self::get_lti_tools())
* @param int $userid The user id
* @return bool|string returns true if successful, else an error code
*/
public static function enrol_user($tool, $userid) {
global $DB;
// Check if the user enrolment exists.
if (!$DB->record_exists('user_enrolments', array('enrolid' => $tool->enrolid, 'userid' => $userid))) {
// Check if the maximum enrolled limit has been met.
if ($tool->maxenrolled) {
if ($DB->count_records('user_enrolments', array('enrolid' => $tool->enrolid)) >= $tool->maxenrolled) {
return self::ENROLMENT_MAX_ENROLLED;
}
}
// Check if the enrolment has not started.
if ($tool->enrolstartdate && time() < $tool->enrolstartdate) {
return self::ENROLMENT_NOT_STARTED;
}
// Check if the enrolment has finished.
if ($tool->enrolenddate && time() > $tool->enrolenddate) {
return self::ENROLMENT_FINISHED;
}
$timeend = 0;
if ($tool->enrolperiod) {
$timeend = time() + $tool->enrolperiod;
}
// Finally, enrol the user.
$instance = new \stdClass();
$instance->id = $tool->enrolid;
$instance->courseid = $tool->courseid;
$instance->enrol = 'lti';
$instance->status = $tool->status;
$ltienrol = enrol_get_plugin('lti');
// Hack - need to do this to workaround DB caching hack. See MDL-53977.
$timestart = intval(substr(time(), 0, 8) . '00') - 1;
$ltienrol->enrol_user($instance, $userid, null, $timestart, $timeend);
}
return self::ENROLMENT_SUCCESSFUL;
}
/**
* Returns the LTI tool.
*
* @param int $toolid
* @return \stdClass the tool
*/
public static function get_lti_tool($toolid) {
global $DB;
$sql = "SELECT elt.*, e.name, e.courseid, e.status, e.enrolstartdate, e.enrolenddate, e.enrolperiod
FROM {enrol_lti_tools} elt
JOIN {enrol} e
ON elt.enrolid = e.id
WHERE elt.id = :tid";
return $DB->get_record_sql($sql, array('tid' => $toolid), MUST_EXIST);
}
/**
* Returns the LTI tools requested.
*
* @param array $params The list of SQL params (eg. array('columnname' => value, 'columnname2' => value)).
* @param int $limitfrom return a subset of records, starting at this point (optional).
* @param int $limitnum return a subset comprising this many records in total
* @return array of tools
*/
public static function get_lti_tools($params = array(), $limitfrom = 0, $limitnum = 0) {
global $DB;
$sql = "SELECT elt.*, e.name, e.courseid, e.status, e.enrolstartdate, e.enrolenddate, e.enrolperiod
FROM {enrol_lti_tools} elt
JOIN {enrol} e
ON elt.enrolid = e.id";
if ($params) {
$where = "WHERE";
foreach ($params as $colname => $value) {
$sql .= " $where $colname = :$colname";
$where = "AND";
}
}
$sql .= " ORDER BY elt.timecreated";
return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
}
/**
* Returns the number of LTI tools.
*
* @param array $params The list of SQL params (eg. array('columnname' => value, 'columnname2' => value)).
* @return int The number of tools
*/
public static function count_lti_tools($params = array()) {
global $DB;
$sql = "SELECT COUNT(*)
FROM {enrol_lti_tools} elt
JOIN {enrol} e
ON elt.enrolid = e.id";
if ($params) {
$where = "WHERE";
foreach ($params as $colname => $value) {
$sql .= " $where $colname = :$colname";
$where = "AND";
}
}
return $DB->count_records_sql($sql, $params);
}
/**
* Create a IMS POX body request for sync grades.
*
* @param string $source Sourceid required for the request
* @param float $grade User final grade
* @return string
*/
public static function create_service_body($source, $grade) {
return '<?xml version="1.0" encoding="UTF-8"?>
<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
<imsx_POXHeader>
<imsx_POXRequestHeaderInfo>
<imsx_version>V1.0</imsx_version>
<imsx_messageIdentifier>' . (time()) . '</imsx_messageIdentifier>
</imsx_POXRequestHeaderInfo>
</imsx_POXHeader>
<imsx_POXBody>
<replaceResultRequest>
<resultRecord>
<sourcedGUID>
<sourcedId>' . $source . '</sourcedId>
</sourcedGUID>
<result>
<resultScore>
<language>en-us</language>
<textString>' . $grade . '</textString>
</resultScore>
</result>
</resultRecord>
</replaceResultRequest>
</imsx_POXBody>
</imsx_POXEnvelopeRequest>';
}
/**
* Returns the url to launch the lti tool.
*
* @param int $toolid the id of the shared tool
* @return \moodle_url the url to launch the tool
* @since Moodle 3.2
*/
public static function get_launch_url($toolid) {
return new \moodle_url('/enrol/lti/tool.php', array('id' => $toolid));
}
/**
* Returns the name of the lti enrolment instance, or the name of the course/module being shared.
*
* @param \stdClass $tool The lti tool
* @return string The name of the tool
* @since Moodle 3.2
*/
public static function get_name($tool) {
$name = null;
if (empty($tool->name)) {
$toolcontext = \context::instance_by_id($tool->contextid);
$name = $toolcontext->get_context_name();
} else {
$name = $tool->name;
};
return $name;
}
/**
* Returns a description of the course or module that this lti instance points to.
*
* @param \stdClass $tool The lti tool
* @return string A description of the tool
* @since Moodle 3.2
*/
public static function get_description($tool) {
global $DB;
$description = '';
$context = \context::instance_by_id($tool->contextid);
if ($context->contextlevel == CONTEXT_COURSE) {
$course = $DB->get_record('course', array('id' => $context->instanceid));
$description = $course->summary;
} else if ($context->contextlevel == CONTEXT_MODULE) {
$cmid = $context->instanceid;
$cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
$module = $DB->get_record($cm->modname, array('id' => $cm->instance));
$description = $module->intro;
}
return trim(html_to_text($description));
}
/**
* Returns the icon of the tool.
*
* @param \stdClass $tool The lti tool
* @return \moodle_url A url to the icon of the tool
* @since Moodle 3.2
*/
public static function get_icon($tool) {
global $OUTPUT;
return $OUTPUT->favicon();
}
/**
* Returns the url to the cartridge representing the tool.
*
* If you have slash arguments enabled, this will be a nice url ending in cartridge.xml.
* If not it will be a php page with some parameters passed.
*
* @param \stdClass $tool The lti tool
* @return string The url to the cartridge representing the tool
* @since Moodle 3.2
*/
public static function get_cartridge_url($tool) {
global $CFG;
$url = null;
$id = $tool->id;
$token = self::generate_cartridge_token($tool->id);
if ($CFG->slasharguments) {
$url = new \moodle_url('/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml');
} else {
$url = new \moodle_url('/enrol/lti/cartridge.php',
array(
'id' => $id,
'token' => $token
)
);
}
return $url;
}
/**
* Returns the url to the tool proxy registration url.
*
* If you have slash arguments enabled, this will be a nice url ending in cartridge.xml.
* If not it will be a php page with some parameters passed.
*
* @param \stdClass $tool The lti tool
* @return string The url to the cartridge representing the tool
*/
public static function get_proxy_url($tool) {
global $CFG;
$url = null;
$id = $tool->id;
$token = self::generate_proxy_token($tool->id);
if ($CFG->slasharguments) {
$url = new \moodle_url('/enrol/lti/proxy.php/' . $id . '/' . $token . '/');
} else {
$url = new \moodle_url('/enrol/lti/proxy.php',
array(
'id' => $id,
'token' => $token
)
);
}
return $url;
}
/**
* Returns a unique hash for this site and this enrolment instance.
*
* Used to verify that the link to the cartridge has not just been guessed.
*
* @param int $toolid The id of the shared tool
* @return string MD5 hash of combined site ID and enrolment instance ID.
* @since Moodle 3.2
*/
public static function generate_cartridge_token($toolid) {
$siteidentifier = get_site_identifier();
$checkhash = md5($siteidentifier . '_enrol_lti_cartridge_' . $toolid);
return $checkhash;
}
/**
* Returns a unique hash for this site and this enrolment instance.
*
* Used to verify that the link to the proxy has not just been guessed.
*
* @param int $toolid The id of the shared tool
* @return string MD5 hash of combined site ID and enrolment instance ID.
* @since Moodle 3.2
*/
public static function generate_proxy_token($toolid) {
$siteidentifier = get_site_identifier();
$checkhash = md5($siteidentifier . '_enrol_lti_proxy_' . $toolid);
return $checkhash;
}
/**
* Verifies that the given token matches the cartridge token of the given shared tool.
*
* @param int $toolid The id of the shared tool
* @param string $token hash for this site and this enrolment instance
* @return boolean True if the token matches, false if it does not
* @since Moodle 3.2
*/
public static function verify_cartridge_token($toolid, $token) {
return $token == self::generate_cartridge_token($toolid);
}
/**
* Verifies that the given token matches the proxy token of the given shared tool.
*
* @param int $toolid The id of the shared tool
* @param string $token hash for this site and this enrolment instance
* @return boolean True if the token matches, false if it does not
* @since Moodle 3.2
*/
public static function verify_proxy_token($toolid, $token) {
return $token == self::generate_proxy_token($toolid);
}
/**
* Returns the parameters of the cartridge as an associative array of partial xpath.
*
* @param int $toolid The id of the shared tool
* @return array Recursive associative array with partial xpath to be concatenated into an xpath expression
* before setting the value.
* @since Moodle 3.2
*/
protected static function get_cartridge_parameters($toolid) {
global $PAGE, $SITE;
$PAGE->set_context(\context_system::instance());
// Get the tool.
$tool = self::get_lti_tool($toolid);
// Work out the name of the tool.
$title = self::get_name($tool);
$launchurl = self::get_launch_url($toolid);
$launchurl = $launchurl->out(false);
$iconurl = self::get_icon($tool);
$iconurl = $iconurl->out(false);
$securelaunchurl = null;
$secureiconurl = null;
$vendorurl = new \moodle_url('/');
$vendorurl = $vendorurl->out(false);
$description = self::get_description($tool);
// If we are a https site, we can add the launch url and icon urls as secure equivalents.
if (\is_https()) {
$securelaunchurl = $launchurl;
$secureiconurl = $iconurl;
}
return array(
"/cc:cartridge_basiclti_link" => array(
"/blti:title" => $title,
"/blti:description" => $description,
"/blti:extensions" => array(
"/lticm:property[@name='icon_url']" => $iconurl,
"/lticm:property[@name='secure_icon_url']" => $secureiconurl
),
"/blti:launch_url" => $launchurl,
"/blti:secure_launch_url" => $securelaunchurl,
"/blti:icon" => $iconurl,
"/blti:secure_icon" => $secureiconurl,
"/blti:vendor" => array(
"/lticp:code" => $SITE->shortname,
"/lticp:name" => $SITE->fullname,
"/lticp:description" => trim(html_to_text($SITE->summary)),
"/lticp:url" => $vendorurl
)
)
);
}
/**
* Traverses a recursive associative array, setting the properties of the corresponding
* xpath element.
*
* @param \DOMXPath $xpath The xpath with the xml to modify
* @param array $parameters The array of xpaths to search through
* @param string $prefix The current xpath prefix (gets longer the deeper into the array you go)
* @return void
* @since Moodle 3.2
*/
protected static function set_xpath($xpath, $parameters, $prefix = '') {
foreach ($parameters as $key => $value) {
if (is_array($value)) {
self::set_xpath($xpath, $value, $prefix . $key);
} else {
$result = @$xpath->query($prefix . $key);
if ($result) {
$node = $result->item(0);
if ($node) {
if (is_null($value)) {
$node->parentNode->removeChild($node);
} else {
$node->nodeValue = s($value);
}
}
} else {
throw new \coding_exception('Please check your XPATH and try again.');
}
}
}
}
/**
* Create an IMS cartridge for the tool.
*
* @param int $toolid The id of the shared tool
* @return string representing the generated cartridge
* @since Moodle 3.2
*/
public static function create_cartridge($toolid) {
$cartridge = new \DOMDocument();
$cartridge->load(realpath(__DIR__ . '/../xml/imslticc.xml'));
$xpath = new \DOMXpath($cartridge);
$xpath->registerNamespace('cc', 'http://www.imsglobal.org/xsd/imslticc_v1p0');
$parameters = self::get_cartridge_parameters($toolid);
self::set_xpath($xpath, $parameters);
return $cartridge->saveXML();
}
}
@@ -0,0 +1,103 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\admin;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
/**
* The admin_setting_registeredplatforms class, for rendering a table of platforms which have been registered.
*
* This setting is useful for LTI 1.3 only.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_setting_registeredplatforms extends \admin_setting {
/**
* Calls parent::__construct with specific arguments
*/
public function __construct() {
$this->nosave = true;
parent::__construct('enrol_lti_tool_registered_platforms', get_string('registeredplatforms', 'enrol_lti'), '',
'');
}
/**
* Always returns true, does nothing.
*
* @return bool true.
*/
public function get_setting() {
return true;
}
/**
* Always returns true, does nothing.
*
* @return bool true.
*/
public function get_defaultsetting() {
return true;
}
/**
* Always returns '', does not write anything.
*
* @param string|array $data the data
* @return string Always returns ''.
*/
public function write_setting($data) {
return '';
}
/**
* Checks if $query is one of the available external services
*
* @param string $query The string to search for
* @return bool Returns true if found, false if not
*/
public function is_related($query) {
if (parent::is_related($query)) {
return true;
}
$appregistrationrepo = new application_registration_repository();
$registrations = $appregistrationrepo->find_all();
foreach ($registrations as $reg) {
if (stripos($reg->get_name(), $query) !== false) {
return true;
}
}
return false;
}
/**
* Builds the HTML to display the table.
*
* @param string $data Unused
* @param string $query
* @return string
*/
public function output_html($data, $query='') {
global $PAGE;
$appregistrationrepo = new application_registration_repository();
$renderer = $PAGE->get_renderer('enrol_lti');
$return = $renderer->render_admin_setting_registered_platforms($appregistrationrepo->find_all());
return highlight($query, $return);
}
}
@@ -0,0 +1,181 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\entity;
/**
* The ags_info class, instances of which represent grade service information for a resource_link or context.
*
* For information about Assignment and Grade Services 2.0, see https://www.imsglobal.org/spec/lti-ags/v2p0/.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ags_info {
/** @var string Scope for lineitem management, used when a platform allows the tool to create lineitems.*/
private const SCOPES_LINEITEM_MANAGE = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
/** @var string Scope for lineitem reads, used when a tool only grants read access to line items.*/
private const SCOPES_LINEITEM_READONLY = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
/** @var string Scope for reading results.*/
private const SCOPES_RESULT_READONLY = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
/** @var string Scope for posting scores.*/
private const SCOPES_SCORES_POST = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
/** @var \moodle_url|null The service URL used to get/put lineitems, if supported*/
private $lineitemsurl;
/** @var \moodle_url|null The lineitemurl, which is only present when a single lineitem is supported.*/
private $lineitemurl;
/** @var array The array of supported lineitem-related scopes for this service instance.*/
private $lineitemscopes = [];
/** @var string|null The supported result scope for this service instance.*/
private $resultscope = null;
/** @var string|null The supported score scope for this service instance.*/
private $scorescope = null;
/**
* The ags_info constructor.
*
* @param \moodle_url|null $lineitemsurl The service URL used to get/put lineitems, if supported.
* @param \moodle_url|null $lineitemurl The lineitemurl, which is only present when a single lineitem is supported.
* @param array $scopes The array of supported scopes for this service instance.
*/
private function __construct(?\moodle_url $lineitemsurl, ?\moodle_url $lineitemurl, array $scopes) {
// Platforms may support just lineitemurl, just lineitemsurl or both. At least one of the two is required.
if (is_null($lineitemsurl) && is_null($lineitemurl)) {
throw new \coding_exception("Missing lineitem or lineitems URL");
}
$this->lineitemsurl = $lineitemsurl;
$this->lineitemurl = $lineitemurl;
$this->validate_scopes($scopes);
}
/**
* Factory method to create a new ags_info instance.
*
* @param \moodle_url|null $lineitemsurl The service URL used to get/put lineitems, if supported.
* @param \moodle_url|null $lineitemurl The lineitemurl, which is only present when a single lineitem is supported.
* @param array $scopes The array of supported scopes for this service instance.
* @return ags_info the object instance.
*/
public static function create(?\moodle_url $lineitemsurl = null, ?\moodle_url $lineitemurl = null,
array $scopes = []): ags_info {
return new self($lineitemsurl, $lineitemurl, $scopes);
}
/**
* Check the supplied scopes for validity and set instance vars if appropriate.
*
* @param array $scopes the array of string scopes to check.
* @throws \coding_exception if any of the scopes is invalid.
*/
private function validate_scopes(array $scopes): void {
$supportedscopes = [
self::SCOPES_LINEITEM_READONLY,
self::SCOPES_LINEITEM_MANAGE,
self::SCOPES_RESULT_READONLY,
self::SCOPES_SCORES_POST
];
foreach ($scopes as $scope) {
if (!is_string($scope)) {
throw new \coding_exception('Scope must be a string value');
}
$key = array_search($scope, $supportedscopes);
if ($key === 0) {
$this->lineitemscopes[] = self::SCOPES_LINEITEM_READONLY;
} else if ($key === 1) {
$this->lineitemscopes[] = self::SCOPES_LINEITEM_MANAGE;
} else if ($key === 2) {
$this->resultscope = self::SCOPES_RESULT_READONLY;
} else if ($key === 3) {
$this->scorescope = self::SCOPES_SCORES_POST;
}
}
}
/**
* Get the url for querying line items, if supported.
*
* @return \moodle_url the url.
*/
public function get_lineitemsurl(): ?\moodle_url {
return $this->lineitemsurl;
}
/**
* Get the single line item url, in cases where only one line item exists.
*
* @return \moodle_url|null the url, or null if not present.
*/
public function get_lineitemurl(): ?\moodle_url {
return $this->lineitemurl;
}
/**
* Get the authorization scope for lineitems.
*
* @return array|null the scopes, if present, else null.
*/
public function get_lineitemscope(): ?array {
return !empty($this->lineitemscopes) ? $this->lineitemscopes : null;
}
/**
* Get the authorization scope for results.
*
* @return string|null the scope, if present, else null.
*/
public function get_resultscope(): ?string {
return $this->resultscope;
}
/**
* Get the authorization scope for scores.
*
* @return string|null the scope, if present, else null.
*/
public function get_scorescope(): ?string {
return $this->scorescope;
}
/**
* Get all supported scopes for this service.
*
* @return string[] the array of supported scopes.
*/
public function get_scopes(): array {
$scopes = [];
foreach ($this->lineitemscopes as $lineitemscope) {
$scopes[] = $lineitemscope;
}
if (!empty($this->resultscope)) {
$scopes[] = $this->resultscope;
}
if (!empty($this->scorescope)) {
$scopes[] = $this->scorescope;
}
return $scopes;
}
}
@@ -0,0 +1,329 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\entity;
/**
* Class application_registration.
*
* This class represents an LTI Advantage Application Registration.
* Each registered application may contain one or more deployments of the Moodle tool.
* This registration provides the security contract for all tool deployments belonging to the registration.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class application_registration {
/** @var int|null the if of this registration instance, or null if it hasn't been stored yet. */
private $id;
/** @var string the name of the application being registered. */
private $name;
/** @var \moodle_url the issuer identifying the platform, as provided by the platform. */
private $platformid;
/** @var string the client id as provided by the platform. */
private $clientid;
/** @var \moodle_url the authentication request URL, as provided by the platform. */
private $authenticationrequesturl;
/** @var \moodle_url the certificate URL, as provided by the platform. */
private $jwksurl;
/** @var \moodle_url the access token URL, as provided by the platform. */
private $accesstokenurl;
/** @var string a unique identifier used by the registration in the initiate_login_uri to act as registration identifier.*/
private $uniqueid;
/** @var int status of the registration, either incomplete (draft) or complete (all required data present). */
private $status;
/** @var int const representing the incomplete state */
const REGISTRATION_STATUS_INCOMPLETE = 0;
/** @var int const representing a complete state */
const REGISTRATION_STATUS_COMPLETE = 1;
/**
* The application_registration constructor.
*
* @param string $name the descriptor for this application registration.
* @param string $uniqueid a unique identifier for the registration used in place of client_id in the login URI.
* @param \moodle_url|null $platformid the URL of application
* @param string|null $clientid unique id for the client on the application
* @param \moodle_url|null $authenticationrequesturl URL to send OIDC Auth requests to.
* @param \moodle_url|null $jwksurl URL to use to get public keys from the application.
* @param \moodle_url|null $accesstokenurl URL to use to get an access token from the application, used in service calls.
* @param int|null $id the id of the object instance, if being created from an existing store item.
*/
private function __construct(string $name, string $uniqueid, ?\moodle_url $platformid, ?string $clientid,
?\moodle_url $authenticationrequesturl, ?\moodle_url $jwksurl, ?\moodle_url $accesstokenurl, int $id = null) {
if (empty($name)) {
throw new \coding_exception("Invalid 'name' arg. Cannot be an empty string.");
}
if (empty($uniqueid)) {
throw new \coding_exception("Invalid 'uniqueid' arg. Cannot be an empty string.");
}
// Resolve the registration status.
$iscomplete = (!is_null($platformid) && !is_null($clientid) && !is_null($authenticationrequesturl) &&
!is_null($authenticationrequesturl) && !is_null($jwksurl) && !is_null($accesstokenurl));
$this->status = $iscomplete ? self::REGISTRATION_STATUS_COMPLETE : self::REGISTRATION_STATUS_INCOMPLETE;
$this->name = $name;
$this->uniqueid = $uniqueid;
$this->platformid = $platformid;
$this->clientid = $clientid;
$this->authenticationrequesturl = $authenticationrequesturl;
$this->jwksurl = $jwksurl;
$this->accesstokenurl = $accesstokenurl;
$this->id = $id;
}
/**
* Factory method to create a new instance of an application registration
*
* @param string $name the descriptor for this application registration.
* @param string $uniqueid a unique identifier for the registration used in place of client_id in the login URI.
* @param \moodle_url $platformid the URL of application
* @param string $clientid unique id for the client on the application
* @param \moodle_url $authenticationrequesturl URL to send OIDC Auth requests to.
* @param \moodle_url $jwksurl URL to use to get public keys from the application.
* @param \moodle_url $accesstokenurl URL to use to get an access token from the application, used in service calls.
* @param int|null $id the id of the object instance, if being created from an existing store item.
* @return application_registration the application_registration instance.
* @throws \coding_exception if an invalid clientid is provided.
*/
public static function create(string $name, string $uniqueid, \moodle_url $platformid, string $clientid,
\moodle_url $authenticationrequesturl, \moodle_url $jwksurl, \moodle_url $accesstokenurl,
int $id = null): application_registration {
if (empty($clientid)) {
throw new \coding_exception("Invalid 'clientid' arg. Cannot be an empty string.");
}
return new self($name, $uniqueid, $platformid, $clientid, $authenticationrequesturl, $jwksurl, $accesstokenurl, $id);
}
/**
* Factory method to create a draft application registration.
*
* @param string $name the descriptor for the draft application registration.
* @param string $uniqueid a unique identifier for the registration used in place of client_id in the login URI.
* @param int|null $id the id of the object instance, if being created from an existing store item.
* @return application_registration the application_registration instance.
*/
public static function create_draft(string $name, string $uniqueid, int $id = null): application_registration {
return new self($name, $uniqueid, null, null, null, null, null, $id);
}
/**
* Get the integer id of this object instance.
*
* Will return null if the instance has not yet been stored.
*
* @return null|int the id, if set, otherwise null.
*/
public function get_id(): ?int {
return $this->id;
}
/**
* Get the name of the application being registered.
*
* @return string the name.
*/
public function get_name(): string {
return $this->name;
}
/**
* Sets the name of this registration.
*
* @param string $name the new name to set.
* @throws \coding_exception if the provided name is invalid.
*/
public function set_name(string $name): void {
if (empty($name)) {
throw new \coding_exception("Invalid 'name' arg. Cannot be an empty string.");
}
$this->name = $name;
}
/**
* Return the local unique client id of the registration.
*
* @return string the id.
*/
public function get_uniqueid(): string {
return $this->uniqueid;
}
/**
* Get the platform id.
*
* @return \moodle_url|null the platformid/issuer URL.
*/
public function get_platformid(): ?\moodle_url {
return $this->platformid;
}
/**
* Sets the platformid/issuer for this registration.
*
* @param \moodle_url $platformid the platform id / iss to set.
*/
public function set_platformid(\moodle_url $platformid): void {
$this->platformid = $platformid;
}
/**
* Get the client id.
*
* @return string|null the client id.
*/
public function get_clientid(): ?string {
return $this->clientid;
}
/**
* Sets the client id for this registration.
*
* @param string $clientid the client id
* @throws \coding_exception if the client id is invalid.
*/
public function set_clientid(string $clientid): void {
if (empty($clientid)) {
throw new \coding_exception("Invalid 'clientid' arg. Cannot be an empty string.");
}
$this->clientid = $clientid;
}
/**
* Get the authentication request URL.
*
* @return \moodle_url|null the authentication request URL.
*/
public function get_authenticationrequesturl(): ?\moodle_url {
return $this->authenticationrequesturl;
}
/**
* Sets the authentication request URL for this registration.
*
* @param \moodle_url $authenticationrequesturl the authentication request URL.
*/
public function set_authenticationrequesturl(\moodle_url $authenticationrequesturl): void {
$this->authenticationrequesturl = $authenticationrequesturl;
}
/**
* Get the JWKS URL.
*
* @return \moodle_url|null the JWKS URL.
*/
public function get_jwksurl(): ?\moodle_url {
return $this->jwksurl;
}
/**
* Sets the JWKS URL for this registration.
*
* @param \moodle_url $jwksurl the JWKS URL.
*/
public function set_jwksurl(\moodle_url $jwksurl): void {
$this->jwksurl = $jwksurl;
}
/**
* Get the access token URL.
*
* @return \moodle_url|null the access token URL.
*/
public function get_accesstokenurl(): ?\moodle_url {
return $this->accesstokenurl;
}
/**
* Sets the access token URL for this registration.
*
* @param \moodle_url $accesstokenurl the access token URL.
*/
public function set_accesstokenurl(\moodle_url $accesstokenurl): void {
$this->accesstokenurl = $accesstokenurl;
}
/**
* Add a tool deployment to this registration.
*
* @param string $name human readable name for the deployment.
* @param string $deploymentid the unique id of the tool deployment in the platform.
* @return deployment the new deployment.
* @throws \coding_exception if trying to add a deployment to an instance without an id assigned.
*/
public function add_tool_deployment(string $name, string $deploymentid): deployment {
if (empty($this->get_id())) {
throw new \coding_exception("Can't add deployment to a resource_link that hasn't first been saved.");
}
return deployment::create(
$this->get_id(),
$deploymentid,
$name
);
}
/**
* Check whether this registration is complete or not.
*/
public function is_complete(): bool {
return $this->status == self::REGISTRATION_STATUS_COMPLETE;
}
/**
* Attempt to progress this registration to the 'complete' state, provided required state exists.
*
* @see REGISTRATION_STATUS_COMPLETE
*
* @throws \coding_exception if the registration isn't in a state to be transitioned to complete.
*/
public function complete_registration(): void {
// Check completeness of registration.
if (is_null($this->platformid)) {
throw new \coding_exception("Unable to complete registration. Platform ID is missing.");
}
if (is_null($this->clientid)) {
throw new \coding_exception("Unable to complete registration. Client ID is missing.");
}
if (is_null($this->accesstokenurl)) {
throw new \coding_exception("Unable to complete registration. Access token URL is missing.");
}
if (is_null($this->authenticationrequesturl)) {
throw new \coding_exception("Unable to complete registration. Authentication request URL is missing.");
}
if (is_null($this->jwksurl)) {
throw new \coding_exception("Unable to complete registration. JWKS URL is missing.");
}
$this->status = self::REGISTRATION_STATUS_COMPLETE;
}
}
@@ -0,0 +1,179 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\entity;
/**
* Class context, instances of which represent a context in the platform.
*
* See: http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary for supported context types.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */
class context {
// The following full contexts are per the spec:
// http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary.
/** @var string course template context */
private const CONTEXT_TYPE_COURSE_TEMPLATE = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseTemplate';
/** @var string course offering context */
private const CONTEXT_TYPE_COURSE_OFFERING = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering';
/** @var string course section context */
private const CONTEXT_TYPE_COURSE_SECTION = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection';
/** @var string group context */
private const CONTEXT_TYPE_GROUP = 'http://purl.imsglobal.org/vocab/lis/v2/course#Group';
// The following simple names are deprecated but are still supported in 1.3 for backwards compatibility.
// http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary.
/** @var string deprecated simple course template context */
private const LEGACY_CONTEXT_TYPE_COURSE_TEMPLATE = 'CourseTemplate';
/** @var string deprecated simple course offering context */
private const LEGACY_CONTEXT_TYPE_COURSE_OFFERING = 'CourseOffering';
/** @var string deprecated simple course section context */
private const LEGACY_CONTEXT_TYPE_COURSE_SECTION = 'CourseSection';
/** @var string deprecated simple group context */
private const LEGACY_CONTEXT_TYPE_GROUP = 'Group';
/** @var int the local id of the deployment instance to which this context belongs. */
private $deploymentid;
/** @var string the contextid as supplied by the platform. */
private $contextid;
/** @var int|null the local id of this object instance, which can be null if the object hasn't been stored before */
private $id;
/** @var string[] the array of context types */
private $types;
/**
* Private constructor.
*
* @param int $deploymentid the local id of the deployment instance to which this context belongs.
* @param string $contextid the context id string, as provided by the platform during launch.
* @param array $types an array of string context types, as provided by the platform during launch.
* @param int|null $id local id of this object instance, nullable for new objects.
*/
private function __construct(int $deploymentid, string $contextid, array $types, ?int $id) {
if (!is_null($id) && $id <= 0) {
throw new \coding_exception('id must be a positive int');
}
$this->deploymentid = $deploymentid;
$this->contextid = $contextid;
$this->set_types($types); // Handles type validation.
$this->id = $id;
}
/**
* Factory method for creating a context instance.
*
* @param int $deploymentid the local id of the deployment instance to which this context belongs.
* @param string $contextid the context id string, as provided by the platform during launch.
* @param array $types an array of string context types, as provided by the platform during launch.
* @param int|null $id local id of this object instance, nullable for new objects.
* @return context the context instance.
*/
public static function create(int $deploymentid, string $contextid, array $types, int $id = null): context {
return new self($deploymentid, $contextid, $types, $id);
}
/**
* Check whether a context is valid or not, checking also deprecated but supported legacy context names.
*
* @param string $type context type to check.
* @param bool $includelegacy whether to check the legacy simple context names too.
* @return bool true if the type is valid, false otherwise.
*/
private function is_valid_type(string $type, bool $includelegacy = false): bool {
// Check LTI Advantage types.
$valid = in_array($type, [
self::CONTEXT_TYPE_COURSE_TEMPLATE,
self::CONTEXT_TYPE_COURSE_OFFERING,
self::CONTEXT_TYPE_COURSE_SECTION,
self::CONTEXT_TYPE_GROUP
]);
// Check legacy short names.
if ($includelegacy) {
$valid = $valid || in_array($type, [
self::LEGACY_CONTEXT_TYPE_COURSE_TEMPLATE,
self::LEGACY_CONTEXT_TYPE_COURSE_OFFERING,
self::LEGACY_CONTEXT_TYPE_COURSE_SECTION,
self::LEGACY_CONTEXT_TYPE_GROUP
]);
}
return $valid;
}
/**
* Get the object instance id.
*
* @return int|null the id, or null if the object doesn't yet have one assigned.
*/
public function get_id(): ?int {
return $this->id;
}
/**
* Return the platform contextid string.
*
* @return string the id of the context in the platform.
*/
public function get_contextid(): string {
return $this->contextid;
}
/**
* Get the id of the local deployment instance to which this context instance belongs.
*
* @return int the id of the local deployment instance to which this context instance belongs.
*/
public function get_deploymentid(): int {
return $this->deploymentid;
}
/**
* Get the context types this context instance represents.
*
* @return string[] the array of context types this context instance represents.
*/
public function get_types(): array {
return $this->types;
}
/**
* Set the list of types this context instance represents.
*
* @param array $types the array of string types.
* @throws \coding_exception if any of the supplied types are invalid.
*/
public function set_types(array $types): void {
foreach ($types as $type) {
if (!$this->is_valid_type($type, true)) {
throw new \coding_exception("Cannot set invalid context type '{$type}'.");
}
}
$this->types = $types;
}
}
@@ -0,0 +1,178 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\entity;
/**
* Class deployment.
*
* This class represents an LTI Advantage Tool Deployment (http://www.imsglobal.org/spec/lti/v1p3/#tool-deployment).
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class deployment {
/** @var int|null the id of this object instance, or null if it has not been saved yet. */
private $id;
/** @var string the name of this deployment. */
private $deploymentname;
/** @var string The platform-issued deployment id. */
private $deploymentid;
/** @var int the local ID of the application registration to which this deployment belongs. */
private $registrationid;
/** @var string|null the legacy consumer key, if the deployment instance is migrated from a legacy consumer. */
private $legacyconsumerkey;
/**
* The private deployment constructor.
*
* @param string $deploymentname the name of this deployment.
* @param string $deploymentid the platform-issued deployment id.
* @param int $registrationid the local ID of the application registration.
* @param int|null $id the id of this object instance, or null if it is a new instance which has not yet been saved.
* @param string|null $legacyconsumerkey the 1.1 consumer key associated with this deployment, used for upgrades.
*/
private function __construct(string $deploymentname, string $deploymentid, int $registrationid, ?int $id = null,
?string $legacyconsumerkey = null) {
if (!is_null($id) && $id <= 0) {
throw new \coding_exception('id must be a positive int');
}
if (empty($deploymentname)) {
throw new \coding_exception("Invalid 'deploymentname' arg. Cannot be an empty string.");
}
if (empty($deploymentid)) {
throw new \coding_exception("Invalid 'deploymentid' arg. Cannot be an empty string.");
}
$this->deploymentname = $deploymentname;
$this->deploymentid = $deploymentid;
$this->registrationid = $registrationid;
$this->id = $id;
$this->legacyconsumerkey = $legacyconsumerkey;
}
/**
* Factory method to create a new instance of a deployment.
*
* @param int $registrationid the local ID of the application registration.
* @param string $deploymentid the platform-issued deployment id.
* @param string $deploymentname the name of this deployment.
* @param int|null $id optional local id of this object instance, omitted for new deployment objects.
* @param string|null $legacyconsumerkey the 1.1 consumer key associated with this deployment, used for upgrades.
* @return deployment the deployment instance.
*/
public static function create(int $registrationid, string $deploymentid, string $deploymentname,
?int $id = null, ?string $legacyconsumerkey = null): deployment {
return new self($deploymentname, $deploymentid, $registrationid, $id, $legacyconsumerkey);
}
/**
* Return the object id.
*
* @return int|null the id.
*/
public function get_id(): ?int {
return $this->id;
}
/**
* Return the short name of this tool deployment.
*
* @return string the short name.
*/
public function get_deploymentname(): string {
return $this->deploymentname;
}
/**
* Get the deployment id string.
*
* @return string deploymentid
*/
public function get_deploymentid(): string {
return $this->deploymentid;
}
/**
* Get the id of the application_registration.
*
* @return int the id of the application_registration instance to which this deployment belongs.
*/
public function get_registrationid(): int {
return $this->registrationid;
}
/**
* Get the legacy consumer key to which this deployment instance is mapped.
*
* @return string|null the legacy consumer key, if set, else null.
*/
public function get_legacy_consumer_key(): ?string {
return $this->legacyconsumerkey;
}
/**
* Factory method to add a platform-specific context to the deployment.
*
* @param string $contextid the contextid, as supplied by the platform during launch.
* @param array $types the context types the context represents, as supplied by the platform during launch.
* @return context the context instance.
* @throws \coding_exception if the context could not be created.
*/
public function add_context(string $contextid, array $types): context {
if (!$this->get_id()) {
throw new \coding_exception('Can\'t add context to a deployment that hasn\'t first been saved');
}
return context::create($this->get_id(), $contextid, $types);
}
/**
* Factory method to create a resource link from this deployment instance.
*
* @param string $resourcelinkid the platform-issued string id of the resource link.
* @param int $resourceid the local published resource to which this link points.
* @param int|null $contextid the platform context instance in which the resource link resides, if available.
* @return resource_link the resource_link instance.
* @throws \coding_exception if the resource_link can't be created.
*/
public function add_resource_link(string $resourcelinkid, int $resourceid,
int $contextid = null): resource_link {
if (!$this->get_id()) {
throw new \coding_exception('Can\'t add resource_link to a deployment that hasn\'t first been saved');
}
return resource_link::create($resourcelinkid, $this->get_id(), $resourceid, $contextid);
}
/**
* Set the legacy consumer key for this instance, indicating that the deployment has been migrated from a consumer.
*
* @param string $key the legacy consumer key.
* @throws \coding_exception if the key is invalid.
*/
public function set_legacy_consumer_key(string $key): void {
if (strlen($key) > 255) {
throw new \coding_exception('Legacy consumer key too long. Cannot exceed 255 chars.');
}
$this->legacyconsumerkey = $key;
}
}
@@ -0,0 +1,191 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\entity;
use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository;
/**
* The migration_claim class, instances of which represent information passed in an 'lti1p1' migration claim.
*
* Provides validation and data retrieval for the claim.
*
* See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class migration_claim {
/** @var string the LTI 1.1 consumer key */
private $consumerkey;
/** @var string the LTI 1.1 user identifier.
* This is only included in the claim if it differs to the value included in the LTI 1.3 'sub' claim.
* I.e. https://www.imsglobal.org/spec/security/v1p0#id-token
*/
private $userid = null;
/** @var string the LTI 1.1 context identifier.
* This is only included in the claim if it differs to the 'id' property of the LTI 1.3 'context' claim.
* I.e. https://purl.imsglobal.org/spec/lti/claim/context#id.
*/
private $contextid = null;
/** @var string the LTI 1.1 consumer instance GUID.
* This is only included in the claim if it differs to the 'guid' property of the LTI 1.3 'tool_platform' claim.
* I.e. https://purl.imsglobal.org/spec/lti/claim/tool_platform#guid.
*/
private $toolconsumerinstanceguid = null;
/** @var string the LTI 1.1 resource link identifier.
* This is only included in the claim if it differs to the 'id' property of the LTI 1.3 'resource_link' claim.
* I.e. https://purl.imsglobal.org/spec/lti/claim/resource_link#id.
*/
private $resourcelinkid = null;
/** @var legacy_consumer_repository repository instance for querying consumer secrets when verifying signature. */
private $legacyconsumerrepo;
/**
* The migration_claim constructor.
*
* @param array $claim the array of claim data, as received in a resource link launch.
* @param string $deploymentid the deployment id included in the launch.
* @param string $platform the platform included in the launch.
* @param string $clientid the client id included in the launch.
* @param string $exp the exp included in the launch.
* @param string $nonce the nonce included in the launch.
* @param legacy_consumer_repository $legacyconsumerrepo a legacy consumer repository instance.
* @throws \coding_exception if the claim data is invalid.
*/
public function __construct(array $claim, string $deploymentid, string $platform, string $clientid, string $exp,
string $nonce, legacy_consumer_repository $legacyconsumerrepo) {
// The oauth_consumer_key property MUST be sent.
// See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key.
if (empty($claim['oauth_consumer_key'])) {
throw new \coding_exception("Missing 'oauth_consumer_key' property in lti1p1 migration claim.");
}
// The oauth_consumer_key_sign property MAY be sent.
// For user migration to take place, however, this is deemed a required property.
// See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign.
if (empty($claim['oauth_consumer_key_sign'])) {
throw new \coding_exception("Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim.");
}
$this->legacyconsumerrepo = $legacyconsumerrepo;
if (!$this->verify_signature($claim['oauth_consumer_key'], $claim['oauth_consumer_key_sign'], $deploymentid,
$platform, $clientid, $exp, $nonce, $legacyconsumerrepo)) {
throw new \coding_exception("Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim.");
}
$this->consumerkey = $claim['oauth_consumer_key'];
$this->userid = $claim['user_id'] ?? null;
$this->contextid = $claim['context_id'] ?? null;
$this->toolconsumerinstanceguid = $claim['tool_consumer_instance_guid'] ?? null;
$this->resourcelinkid = $claim['resource_link_id'] ?? null;
}
/**
* Verify the claim signature by recalculating it using the launch data and locally stored consumer secret.
*
* @param string $consumerkey the LTI 1.1 consumer key.
* @param string $signature a signature of the LTI 1.1 consumer key and associated launch data.
* @param string $deploymentid the deployment id included in the launch.
* @param string $platform the platform included in the launch.
* @param string $clientid the client id included in the launch.
* @param string $exp the exp included in the launch.
* @param string $nonce the nonce included in the launch.
* @return bool true if the signature was verified, false otherwise.
*/
private function verify_signature(string $consumerkey, string $signature, string $deploymentid, string $platform,
string $clientid, string $exp, string $nonce): bool {
$base = [
$consumerkey,
$deploymentid,
$platform,
$clientid,
$exp,
$nonce
];
$basestring = implode('&', $base);
// Legacy enrol_lti code permits tools to share a consumer key but use different secrets. This results in
// potentially many secrets per mapped tool consumer. As such, when generating the migration claim it's
// impossible to know which secret the platform will use to sign the consumer key. The consumer key in the
// migration claim is thus verified by trying all known secrets for the consumer, until either a match is found
// or no signatures match.
$consumersecrets = $this->legacyconsumerrepo->get_consumer_secrets($consumerkey);
foreach ($consumersecrets as $consumersecret) {
$calculatedsignature = base64_encode(hash_hmac('sha256', $basestring, $consumersecret));
if ($signature === $calculatedsignature) {
return true;
}
}
return false;
}
/**
* Return the consumer key stored in the claim.
*
* @return string the consumer key included in the claim.
*/
public function get_consumer_key(): string {
return $this->consumerkey;
}
/**
* Return the LTI 1.1 user id stored in the claim.
*
* @return string|null the user id, or null if not provided in the claim.
*/
public function get_user_id(): ?string {
return $this->userid;
}
/**
* Return the LTI 1.1 context id stored in the claim.
*
* @return string|null the context id, or null if not provided in the claim.
*/
public function get_context_id(): ?string {
return $this->contextid;
}
/**
* Return the LTI 1.1 tool consumer instance GUID stored in the claim.
*
* @return string|null the tool consumer instance GUID, or null if not provided in the claim.
*/
public function get_tool_consumer_instance_guid(): ?string {
return $this->toolconsumerinstanceguid;
}
/**
* Return the LTI 1.1 resource link id stored in the claim.
*
* @return string|null the resource link id, or null if not provided in the claim.
*/
public function get_resource_link_id(): ?string {
return $this->resourcelinkid;
}
}
@@ -0,0 +1,131 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\entity;
/**
* Class nrps_info, instances of which represent a names and roles provisioning service for a resource.
*
* For information about Names and Role Provisioning Services 2.0, see http://www.imsglobal.org/spec/lti-nrps/v2p0.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */
class nrps_info {
/** @var \moodle_url the memberships URL for the service. */
private $contextmembershipsurl;
/** @var float[] the array of supported service versions. */
private $serviceversions;
// Service versions are specified by the platform during launch.
// See http://www.imsglobal.org/spec/lti-nrps/v2p0#lti-1-3-integration.
/** @var string version 1.0 */
private const SERVICE_VERSION_1 = '1.0';
/** @var string version 2.0 */
private const SERVICE_VERSION_2 = '2.0';
// Scope that must be requested as part of making a service call.
// See: http://www.imsglobal.org/spec/lti-nrps/v2p0#lti-1-3-integration.
/** @var string the scope to request to make service calls. */
private $servicescope = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
/**
* The private nrps_info constructor.
*
* @param \moodle_url $contextmembershipsurl the memberships URL.
* @param string[] $serviceversions the supported service versions.
*/
private function __construct(\moodle_url $contextmembershipsurl, array $serviceversions = [self::SERVICE_VERSION_2]) {
$this->contextmembershipsurl = $contextmembershipsurl;
$this->set_service_versions($serviceversions);
}
/**
* Factory method to create a new nrps_info instance.
*
* @param \moodle_url $contextmembershipsurl the memberships URL.
* @param string[] $serviceversions the supported service versions.
* @return nrps_info the object instance.
*/
public static function create(\moodle_url $contextmembershipsurl,
array $serviceversions = [self::SERVICE_VERSION_2]): nrps_info {
return new self($contextmembershipsurl, $serviceversions);
}
/**
* Check whether the supplied service version is valid or not.
*
* @param string $serviceversion the service version to check.
* @return bool true if valid, false otherwise.
*/
private function is_valid_service_version(string $serviceversion): bool {
$validversions = [
self::SERVICE_VERSION_1,
self::SERVICE_VERSION_2
];
return in_array($serviceversion, $validversions);
}
/**
* Tries to set the supported service versions for this instance.
*
* @param array $serviceversions the service versions to set.
* @throws \coding_exception if any of the supplied versions are not valid.
*/
private function set_service_versions(array $serviceversions): void {
if (empty($serviceversions)) {
throw new \coding_exception('Service versions array cannot be empty');
}
$serviceversions = array_unique($serviceversions);
foreach ($serviceversions as $serviceversion) {
if (!$this->is_valid_service_version($serviceversion)) {
throw new \coding_exception("Invalid Names and Roles service version '{$serviceversion}'");
}
}
$this->serviceversions = $serviceversions;
}
/**
* Get the service URL for this grade service instance.
*
* @return \moodle_url the service URL.
*/
public function get_context_memberships_url(): \moodle_url {
return clone $this->contextmembershipsurl;
}
/**
* Get the supported service versions for this grade service instance.
*
* @return string[] the array of supported service versions.
*/
public function get_service_versions(): array {
return $this->serviceversions;
}
/**
* Get the nrps service scope.
*
* @return string the service scope.
*/
public function get_service_scope(): string {
return $this->servicescope;
}
}
@@ -0,0 +1,231 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\entity;
/**
* Class resource_link.
*
* This class represents an LTI Advantage Resource Link (http://www.imsglobal.org/spec/lti/v1p3/#resource-link).
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class resource_link {
/** @var int|null the id of this object, or null if the object hasn't been stored yet. */
private $id;
/** @var string resourcelinkid the id of the resource link as supplied by the platform. */
private $resourcelinkid;
/** @var int $deploymentid the local id of the deployment instance to which this resource link belongs. */
private $deploymentid;
/** @var int|null $contextid the id of local context object representing the platform context, or null. */
private $contextid;
/** @var int The id of the local published resource this resource_link points to. */
private $resourceid;
/** @var ags_info|null the grade service for this resource_link, null if not applicable/not provided. */
private $gradeservice;
/** @var nrps_info|null the names and roles service for this resource_link, null if not applicable/not provided. */
private $namesrolesservice;
/**
* The private resource_link constructor.
*
* @param string $resourcelinkid the id of the resource link as supplied by the platform.
* @param int $deploymentid the local id of the deployment instance to which this resource link belongs.
* @param int $resourceid the id of the local resource to which this link refers.
* @param int|null $contextid the id local context object representing the context within the platform.
* @param int|null $id the local id of this resource_link object.
* @throws \coding_exception if the instance is unable to be created.
*/
private function __construct(string $resourcelinkid, int $deploymentid, int $resourceid, ?int $contextid = null,
int $id = null) {
if (empty($resourcelinkid)) {
throw new \coding_exception('Error: resourcelinkid cannot be an empty string');
}
$this->resourcelinkid = $resourcelinkid;
$this->deploymentid = $deploymentid;
$this->resourceid = $resourceid;
$this->contextid = $contextid;
$this->id = $id;
$this->gradeservice = null;
$this->namesrolesservice = null;
}
/**
* Factory method to create an instance.
*
* @param string $resourcelinkid the resourcelinkid, as provided by the platform.
* @param int $deploymentid the local id of the deployment to which this resource link belongs.
* @param int $resourceid the id of the local resource this resource_link refers to.
* @param int|null $contextid the id of the local context object representing the platform context.
* @param int|null $id the local id of the resource link instance.
* @return resource_link the newly created instance.
*/
public static function create(string $resourcelinkid, int $deploymentid, int $resourceid, ?int $contextid = null,
int $id = null): resource_link {
return new self($resourcelinkid, $deploymentid, $resourceid, $contextid, $id);
}
/**
* Return the id of this object instance.
*
* @return null|int the id or null if the object has not yet been stored.
*/
public function get_id(): ?int {
return $this->id;
}
/**
* Get the resourcelinkid as provided by the platform.
*
* @return string the resourcelinkid.
*/
public function get_resourcelinkid(): string {
return $this->resourcelinkid;
}
/**
* Return the id of the deployment to which this resource link belongs.
*
* This id is the local id of the deployment instance, not the deploymentid provided by the platform.
*
* @return int the deployment id.
*/
public function get_deploymentid(): int {
return $this->deploymentid;
}
/**
* Get the local id of the published resource to which this resource link refers.
*
* @return int the id of the published resource.
*/
public function get_resourceid(): int {
return $this->resourceid;
}
/**
* Return the id of the context object holding information about where this resource link resides.
*
* @return int|null the id or null if not present.
*/
public function get_contextid(): ?int {
return $this->contextid;
}
/**
* Link this resource_link instance with a context.
*
* @param int $contextid the local id of the context instance containing information about the platform context.
*/
public function set_contextid(int $contextid): void {
if ($contextid <= 0) {
throw new \coding_exception('Context id must be a positive int');
}
$this->contextid = $contextid;
}
/**
* Set which local published resource this resource link refers to.
*
* @param int $resourceid the published resource id.
*/
public function set_resourceid(int $resourceid): void {
if ($resourceid <= 0) {
throw new \coding_exception('Resource id must be a positive int');
}
$this->resourceid = $resourceid;
}
/**
* Add grade service information to this resource_link instance.
*
* @param \moodle_url|null $lineitemsurl the service URL for get/put of line items, if supported.
* @param \moodle_url|null $lineitemurl the service URL if only a single line item is present in the platform.
* @param string[] $scopes the string array of grade service scopes which may be used by the service.
*/
public function add_grade_service(?\moodle_url $lineitemsurl = null, ?\moodle_url $lineitemurl = null, array $scopes = []) {
$this->gradeservice = ags_info::create($lineitemsurl, $lineitemurl, $scopes);
}
/**
* Get the grade service attached to this resource_link instance, or null if there isn't one associated.
*
* @return ags_info|null the grade service object instance, or null if not found.
*/
public function get_grade_service(): ?ags_info {
return $this->gradeservice;
}
/**
* Add names and roles service information to this resource_link instance.
*
* @param \moodle_url $contextmembershipurl the service URL for memberships.
* @param string[] $serviceversions the string array of supported service versions.
*/
public function add_names_and_roles_service(\moodle_url $contextmembershipurl, array $serviceversions): void {
$this->namesrolesservice = nrps_info::create($contextmembershipurl, $serviceversions);
}
/**
* Get the names and roles service attached to this resource_link instance, or null if there isn't one associated.
*
* @return nrps_info|null the names and roles service object instance, or null if not found.
*/
public function get_names_and_roles_service(): ?nrps_info {
return $this->namesrolesservice;
}
/**
* Factory method to create a user from this resource_link instance.
*
* This is useful for associating the user with the resource link and resource I.e. the user was created when
* launching a specific resource link.
*
* @param int $userid the id of the Moodle user record.
* @param string $sourceid the id of the user on the platform.
* @param string $lang the user's lang code.
* @param string $city the user's city.
* @param string $country the user's country.
* @param string $institution the user's institution.
* @param string $timezone the user's timezone.
* @param int|null $maildisplay the user's maildisplay, which can be omitted to use sensible defaults.
* @return user the user instance.
* @throws \coding_exception if trying to add a user to an as-yet-unsaved resource_link instance.
*/
public function add_user(int $userid, string $sourceid, string $lang,
string $city, string $country, string $institution, string $timezone,
?int $maildisplay = null): user {
if (empty($this->get_id())) {
throw new \coding_exception('Can\'t add user to a resource_link that hasn\'t first been saved');
}
return user::create_from_resource_link($this->get_id(), $this->get_resourceid(), $userid,
$this->get_deploymentid(), $sourceid, $lang, $timezone, $city, $country,
$institution, $maildisplay);
}
}
@@ -0,0 +1,420 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\entity;
/**
* Class user, instances of which represent a specific lti user in the tool.
*
* A user is always associated with a resource, as lti users cannot exist without a tool-published-resource. A user will
* always come from either:
* - a resource link launch or
* - a membership sync
* Both of which required a published resource.
*
* Additionally, a user may be associated with a given resource_link instance, to signify that the user originated from
* that resource_link. If a user is not associated with a resource_link, such as when creating a user during a member
* sync, that param is nullable. This can be achieved via the factory method user::create_from_resource_link() or set
* after instantiation via the user::set_resource_link_id() method.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */
class user {
/** @var int the id of the published resource to which this user belongs. */
private $resourceid;
/** @var int the local id of the deployment instance to which this user belongs. */
private $deploymentid;
/** @var string the id of the user in the platform site. */
private $sourceid;
/** @var int|null the id of this user instance, or null if not stored yet. */
private $id;
/** @var int|null the id of the user in the tool site, or null if the instance hasn't been stored yet. */
private $localid;
/** @var string city of the user. */
private $city;
/** @var string country of the user. */
private $country;
/** @var string institution of the user.*/
private $institution;
/** @var string timezone of the user. */
private $timezone;
/** @var int maildisplay of the user. */
private $maildisplay;
/** @var string language code of the user. */
private $lang;
/** @var float the user's last grade value. */
private $lastgrade;
/** @var int|null the user's last access unix timestamp, or null if they have not accessed the resource yet.*/
private $lastaccess;
/** @var int|null the id of the resource_link instance, or null if the user doesn't originate from one. */
private $resourcelinkid;
/**
* Private constructor.
*
* @param int $resourceid the id of the published resource to which this user belongs.
* @param int $userid the id of the Moodle user to which this LTI user relates.
* @param int $deploymentid the local id of the deployment instance to which this user belongs.
* @param string $sourceid the id of the user in the platform site.
* @param string $lang the user's language code.
* @param string $city the user's city.
* @param string $country the user's country.
* @param string $institution the user's institution.
* @param string $timezone the user's timezone.
* @param int|null $maildisplay the user's maildisplay, or null to select defaults.
* @param float|null $lastgrade the user's last grade value.
* @param int|null $lastaccess the user's last access time, or null if they haven't accessed the resource.
* @param int|null $resourcelinkid the id of the resource link to link to the user, or null if not applicable.
* @param int|null $id the id of this object instance, or null if it's a not-yet-persisted object.
* @throws \coding_exception
*/
private function __construct(int $resourceid, int $userid, int $deploymentid, string $sourceid,
string $lang, string $city, string $country,
string $institution, string $timezone, ?int $maildisplay, ?float $lastgrade, ?int $lastaccess,
?int $resourcelinkid = null, ?int $id = null) {
global $CFG;
$this->resourceid = $resourceid;
$this->localid = $userid;
$this->deploymentid = $deploymentid;
if (empty($sourceid)) {
throw new \coding_exception('Invalid sourceid value. Cannot be an empty string.');
}
$this->sourceid = $sourceid;
$this->set_lang($lang);
$this->set_city($city);
$this->set_country($country);
$this->set_institution($institution);
$this->set_timezone($timezone);
$maildisplay = $maildisplay ?? ($CFG->defaultpreference_maildisplay ?? 2);
$this->set_maildisplay($maildisplay);
$this->lastgrade = $lastgrade ?? 0.0;
$this->lastaccess = $lastaccess;
$this->resourcelinkid = $resourcelinkid;
$this->id = $id;
}
/**
* Factory method for creating a user instance associated with a given resource_link instance.
*
* @param int $resourcelinkid the local id of the resource link instance to link to the user.
* @param int $resourceid the id of the published resource to which this user belongs.
* @param int $userid the id of the Moodle user to which this LTI user relates.
* @param int $deploymentid the local id of the deployment instance to which this user belongs.
* @param string $sourceid the id of the user in the platform site.
* @param string $lang the user's language code.
* @param string $timezone the user's timezone.
* @param string $city the user's city.
* @param string $country the user's country.
* @param string $institution the user's institution.
* @param int|null $maildisplay the user's maildisplay, or null to select defaults.
* @return user the user instance.
*/
public static function create_from_resource_link(int $resourcelinkid, int $resourceid, int $userid,
int $deploymentid, string $sourceid, string $lang, string $timezone,
string $city = '', string $country = '', string $institution = '',
?int $maildisplay = null): user {
return new self($resourceid, $userid, $deploymentid, $sourceid, $lang, $city,
$country, $institution, $timezone, $maildisplay, null, null, $resourcelinkid);
}
/**
* Factory method for creating a user.
*
* @param int $resourceid the id of the published resource to which this user belongs.
* @param int $userid the id of the Moodle user to which this LTI user relates.
* @param int $deploymentid the local id of the deployment instance to which this user belongs.
* @param string $sourceid the id of the user in the platform site.
* @param string $lang the user's language code.
* @param string $timezone the user's timezone.
* @param string $city the user's city.
* @param string $country the user's country.
* @param string $institution the user's institution.
* @param int|null $maildisplay the user's maildisplay, or null to select defaults.
* @param float|null $lastgrade the user's last grade value.
* @param int|null $lastaccess the user's last access time, or null if they haven't accessed the resource.
* @param int|null $resourcelinkid the local id of the resource link instance associated with the user.
* @param int|null $id the id of this lti user instance, or null if it's a not-yet-persisted object.
* @return user the user instance.
*/
public static function create(int $resourceid, int $userid, int $deploymentid, string $sourceid,
string $lang, string $timezone, string $city = '',
string $country = '', string $institution = '', ?int $maildisplay = null, ?float $lastgrade = null,
?int $lastaccess = null, ?int $resourcelinkid = null, int $id = null): user {
return new self($resourceid, $userid, $deploymentid, $sourceid, $lang, $city,
$country, $institution, $timezone, $maildisplay, $lastgrade, $lastaccess, $resourcelinkid, $id);
}
/**
* Get the id of this user instance.
*
* @return int|null the object id, or null if not yet persisted.
*/
public function get_id(): ?int {
return $this->id;
}
/**
* Get the id of the resource_link instance to which this user is associated.
*
* @return int|null the object id, or null if the user isn't associated with one.
*/
public function get_resourcelinkid(): ?int {
return $this->resourcelinkid;
}
/**
* Associate this user with the given resource_link instance, denoting that this user launched from the instance.
*
* @param int $resourcelinkid the id of the resource_link instance.
*/
public function set_resourcelinkid(int $resourcelinkid): void {
if ($resourcelinkid <= 0) {
throw new \coding_exception("Invalid resourcelinkid '$resourcelinkid' provided. Must be > 0.");
}
$this->resourcelinkid = $resourcelinkid;
}
/**
* Get the id of the published resource to which this user is associated.
*
* @return int the resource id.
*/
public function get_resourceid(): int {
return $this->resourceid;
}
/**
* Get the id of the deployment instance to which this user belongs.
*
* @return int id of the deployment instance.
*/
public function get_deploymentid(): int {
return $this->deploymentid;
}
/**
* Get the id of the user in the platform.
*
* @return string the source id.
*/
public function get_sourceid(): string {
return $this->sourceid;
}
/**
* Get the id of the user in the tool.
*
* @return int|null the id, or null if the object instance hasn't yet been persisted.
*/
public function get_localid(): ?int {
return $this->localid;
}
/**
* Get the user's city.
*
* @return string the city.
*/
public function get_city(): string {
return $this->city;
}
/**
* Set the user's city.
*
* @param string $city the city string.
*/
public function set_city(string $city): void {
$this->city = $city;
}
/**
* Get the user's country code.
*
* @return string the country code.
*/
public function get_country(): string {
return $this->country;
}
/**
* Set the user's country.
*
* @param string $countrycode the 2 digit country code representing the country, or '' to denote none.
*/
public function set_country(string $countrycode): void {
global $CFG;
require_once($CFG->libdir . '/moodlelib.php');
$validcountrycodes = array_merge([''], array_keys(get_string_manager()->get_list_of_countries(true)));
if (!in_array($countrycode, $validcountrycodes)) {
throw new \coding_exception("Invalid country code '$countrycode'.");
}
$this->country = $countrycode;
}
/**
* Get the instituation of the user.
*
* @return string the institution.
*/
public function get_institution(): string {
return $this->institution;
}
/**
* Set the user's institution.
*
* @param string $institution the name of the institution.
*/
public function set_institution(string $institution): void {
$this->institution = $institution;
}
/**
* Get the timezone of the user.
*
* @return string the user timezone.
*/
public function get_timezone(): string {
return $this->timezone;
}
/**
* Set the user's timezone, or set '99' to specify server timezone.
*
* @param string $timezone the timezone string, or '99' to use server timezone.
*/
public function set_timezone(string $timezone): void {
if (empty($timezone)) {
throw new \coding_exception('Invalid timezone value. Cannot be an empty string.');
}
$validtimezones = array_keys(\core_date::get_list_of_timezones(null, true));
if (!in_array($timezone, $validtimezones)) {
throw new \coding_exception("Invalid timezone '$timezone' provided.");
}
$this->timezone = $timezone;
}
/**
* Get the maildisplay of the user.
*
* @return int the maildisplay.
*/
public function get_maildisplay(): int {
return $this->maildisplay;
}
/**
* Set the user's mail display preference from a range of supported options.
*
* 0 - hide from non privileged users
* 1 - allow everyone to see
* 2 - allow only course participants to see
*
* @param int $maildisplay the maildisplay preference to set.
*/
public function set_maildisplay(int $maildisplay): void {
if (!in_array($maildisplay, range(0, 2))) {
throw new \coding_exception("Invalid maildisplay value '$maildisplay'. Must be in the range {0..2}.");
}
$this->maildisplay = $maildisplay;
}
/**
* Get the lang code of the user.
*
* @return string the user's language code.
*/
public function get_lang(): string {
return $this->lang;
}
/**
* Set the user's language.
*
* @param string $langcode the language code representing the user's language.
*/
public function set_lang(string $langcode): void {
if (empty($langcode)) {
throw new \coding_exception('Invalid lang value. Cannot be an empty string.');
}
$validlangcodes = array_keys(get_string_manager()->get_list_of_translations());
if (!in_array($langcode, $validlangcodes)) {
throw new \coding_exception("Invalid lang '$langcode' provided.");
}
$this->lang = $langcode;
}
/**
* Get the last grade value for this user.
*
* @return float the float grade.
*/
public function get_lastgrade(): float {
return $this->lastgrade;
}
/**
* Set the last grade for the user.
*
* @param float $lastgrade the last grade the user received.
*/
public function set_lastgrade(float $lastgrade): void {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
$this->lastgrade = grade_floatval($lastgrade);
}
/**
* Get the last access timestamp for this user.
*
* @return int|null the last access timestampt, or null if the user hasn't accessed the resource.
*/
public function get_lastaccess(): ?int {
return $this->lastaccess;
}
/**
* Set the last access time for the user.
*
* @param int $time unix timestamp representing the last time the user accessed the published resource.
* @throws \coding_exception if $time is not a positive int.
*/
public function set_lastaccess(int $time): void {
if ($time < 0) {
throw new \coding_exception('Cannot set negative access time');
}
$this->lastaccess = $time;
}
}
@@ -0,0 +1,47 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* The create_registration_form class, for creating a new pending platform registration.
*
* @package enrol_lti
* @copyright 2022 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class create_registration_form extends \moodleform {
/**
* Define the form.
*/
protected function definition() {
$mform = $this->_form;
// Name.
$mform->addElement('text', 'name', get_string('registerplatform:name', 'enrol_lti'));
$mform->setType('name', PARAM_TEXT);
$mform->addRule('name', get_string('required'), 'required', null, 'client');
$mform->addHelpButton('name', 'registerplatform:name', 'enrol_lti');
// Continue/cancel buttons.
$this->add_action_buttons(true, get_string('continue'));
}
}
@@ -0,0 +1,82 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\form;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* The deployment_form class, for registering a deployment for a registered platform.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class deployment_form extends \moodleform {
/**
* Define the form.
*/
protected function definition() {
$mform = $this->_form;
$strrequired = get_string('required');
// Registration id.
$mform->addElement('hidden', 'registrationid');
$mform->setType('registrationid', PARAM_INT);
// Name.
$mform->addElement('text', 'name', get_string('adddeployment:name', 'enrol_lti'));
$mform->setType('name', PARAM_TEXT);
$mform->addRule('name', $strrequired, 'required', null, 'client');
$mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
// Deployment Id.
$mform->addElement('text', 'deploymentid', get_string('adddeployment:deploymentid', 'enrol_lti'));
$mform->setType('deploymentid', PARAM_TEXT);
$mform->addRule('deploymentid', $strrequired, 'required', null, 'client');
$mform->addRule('deploymentid', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
$mform->addHelpButton('deploymentid', 'adddeployment:deploymentid', 'enrol_lti');
$buttonarray = [];
$buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('savechanges'));
$buttonarray[] = $mform->createElement('cancel');
$mform->addGroup($buttonarray, 'buttonar', '', ' ', false);
}
/**
* Provides uniqueness validation of the deployment id.
*
* @param array $data any form data
* @param array $files any submitted files
* @return array array of errors.
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
// Validate the uniqueness of the deploymentid within the registration.
$deploymentrepo = new deployment_repository();
if ($deploymentrepo->find_by_registration($data['registrationid'], $data['deploymentid'])) {
$errors['deploymentid'] = get_string('adddeployment:invaliddeploymentiderror', 'enrol_lti');
}
return $errors;
}
}
@@ -0,0 +1,115 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\form;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* The platform_registration_form class, for registering a platform as a consumer of a published tool.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class platform_registration_form extends \moodleform {
/**
* Define the form.
*/
protected function definition() {
$mform = $this->_form;
$strrequired = get_string('required');
// Id.
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
// Name.
$mform->addElement('text', 'name', get_string('registerplatform:name', 'enrol_lti'));
$mform->setType('name', PARAM_TEXT);
$mform->addRule('name', $strrequired, 'required', null, 'client');
// Platform Id.
$mform->addElement('text', 'platformid', get_string('registerplatform:platformid', 'enrol_lti'));
$mform->setType('platformid', PARAM_URL);
$mform->addRule('platformid', $strrequired, 'required', null, 'client');
$mform->addHelpButton('platformid', 'registerplatform:platformid', 'enrol_lti');
// Client Id.
$mform->addElement('text', 'clientid', get_string('registerplatform:clientid', 'enrol_lti'));
$mform->setType('clientid', PARAM_TEXT);
$mform->addRule('clientid', $strrequired, 'required', null, 'client');
$mform->addHelpButton('clientid', 'registerplatform:clientid', 'enrol_lti');
// Authentication request URL.
$mform->addElement('text', 'authenticationrequesturl', get_string('registerplatform:authrequesturl', 'enrol_lti'));
$mform->setType('authenticationrequesturl', PARAM_URL);
$mform->addRule('authenticationrequesturl', $strrequired, 'required', null, 'client');
$mform->addHelpButton('authenticationrequesturl', 'registerplatform:authrequesturl', 'enrol_lti');
// JSON Web Key Set URL.
$mform->addElement('text', 'jwksurl', get_string('registerplatform:jwksurl', 'enrol_lti'));
$mform->setType('jwksurl', PARAM_URL);
$mform->addRule('jwksurl', $strrequired, 'required', null, 'client');
$mform->addHelpButton('jwksurl', 'registerplatform:jwksurl', 'enrol_lti');
// Access token URL.
$mform->addElement('text', 'accesstokenurl', get_string('registerplatform:accesstokenurl', 'enrol_lti'));
$mform->setType('accesstokenurl', PARAM_URL);
$mform->addRule('accesstokenurl', $strrequired, 'required', null, 'client');
$mform->addHelpButton('accesstokenurl', 'registerplatform:accesstokenurl', 'enrol_lti');
$buttonarray = [];
$buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('savechanges'));
$buttonarray[] = $mform->createElement('cancel');
$mform->addGroup($buttonarray, 'buttonar', '', ' ', false);
}
/**
* Provides validation of URL syntax and issuer uniqueness.
*
* @param array $data the form data.
* @param array $files any submitted files.
* @return array an array of errors.
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
// Check URLs look ok.
foreach ($data as $key => $val) {
if (isset($this->_form->_types[$key]) && $this->_form->_types[$key] == 'url') {
if (!filter_var($val, FILTER_VALIDATE_URL)) {
$errors[$key] = get_string('registerplatform:invalidurlerror', 'enrol_lti');
}
}
}
// Check uniqueness of the {issuer, client_id} tuple.
$appregistrationrepo = new application_registration_repository();
$appreg = $appregistrationrepo->find_by_platform($data['platformid'], $data['clientid']);
if ($appreg) {
if (empty($data['id']) || $appreg->get_id() != $data['id']) {
$errors['clientid'] = get_string('registerplatform:duplicateregistrationerror', 'enrol_lti');
}
}
return $errors;
}
}
@@ -0,0 +1,120 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\lib;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use Packback\Lti1p3\Interfaces\IDatabase;
use Packback\Lti1p3\LtiDeployment;
use Packback\Lti1p3\LtiRegistration;
/**
* The issuer_database class, providing a read-only store of issuer details.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class issuer_database implements IDatabase {
/** @var application_registration_repository an application registration repository instance used for lookups.*/
private $appregrepo;
/** @var deployment_repository a deployment repository instance for lookups.*/
private $deploymentrepo;
/**
* The issuer_database constructor.
* @param application_registration_repository $appregrepo an application registration repository instance.
* @param deployment_repository $deploymentrepo a deployment repository instance.
*/
public function __construct(application_registration_repository $appregrepo,
deployment_repository $deploymentrepo) {
$this->appregrepo = $appregrepo;
$this->deploymentrepo = $deploymentrepo;
}
/**
* Find and return an LTI registration based on its unique {issuer, client_id} tuple.
*
* @param string $iss the issuer id.
* @param string|null $clientId the client_id of the registration.
* @return LtiRegistration|null The registration object, or null if not found.
*/
public function findRegistrationByIssuer(string $iss, ?string $clientId = null): ?LtiRegistration {
if (is_null($clientId)) {
throw new \coding_exception("The param 'clientid' is required. Calling code must either pass in 'client_id' ".
"(generated by the platform during registration) or 'id' (found in the initiate login URI created by the tool) ".
"to identify the client.");
}
global $CFG;
require_once($CFG->libdir . '/moodlelib.php'); // For get_config() usage.
// We can identify registrations two ways. Either:
// 1. Using issuer + the platform-generated clientid. Most platforms will have sent client_id in the initiate login request
// despite it being an optional param in the spec. They must include it as the aud value in the resource link request JWT.
// 2. Using issuer + a tool-generated ID. This supports platforms which omit client_id during a login call. Using the ID
// that is a part of the initiate login URI allows Moodle to locate the registration for that unique client, without the
// platform-generated client_id.
// Major platforms will likely include client_id in the login request, so favor that approach first, only falling back on
// the local id approach where a registration cannot be found in the first instance.
$reg = $this->appregrepo->find_by_platform($iss, $clientId);
if (!$reg) {
$reg = $this->appregrepo->find_by_platform_uniqueid($iss, $clientId);
}
if (!$reg) {
return null;
}
$privatekey = get_config('enrol_lti', 'lti_13_privatekey');
$kid = get_config('enrol_lti', 'lti_13_kid');
return LtiRegistration::new()
->setAuthLoginUrl($reg->get_authenticationrequesturl()->out(false))
->setAuthTokenUrl($reg->get_accesstokenurl()->out(false))
->setClientId($reg->get_clientid())
->setKeySetUrl($reg->get_jwksurl()->out(false))
->setKid($kid)
->setIssuer($reg->get_platformid()->out(false))
->setToolPrivateKey($privatekey);
}
/**
* Returns an LTI deployment based on the {issuer, client_id} tuple and a deployment id string.
*
* @param string $iss the issuer id.
* @param string $deploymentId the deployment id.
* @param string|null $clientId the client_id of the registration.
* @return LtiDeployment|null The deployment object or null if not found.
*/
public function findDeployment(string $iss, string $deploymentId, ?string $clientId = null): ?LtiDeployment {
if (is_null($clientId)) {
throw new \coding_exception("Both issuer and client id are required to identify platform registrations ".
"and must be included in the 'aud' claim of the message JWT.");
}
$appregistration = $this->appregrepo->find_by_platform($iss, $clientId);
if (!$appregistration) {
return null;
}
$deployment = $this->deploymentrepo->find_by_registration($appregistration->get_id(), $deploymentId);
if (!$deployment) {
return null;
}
return LtiDeployment::new($deployment->get_deploymentid());
}
}
@@ -0,0 +1,120 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\lib;
use Packback\Lti1p3\Interfaces\ICache;
/**
* The launch_cache_session, providing a temporary session store for launch information.
*
* This is used to store the launch information while the user is transitioned through the Moodle authentication flows
* and back to the deep linking launch handler (launch_deeplink.php).
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class launch_cache_session implements ICache {
/**
* Get the launch data from the cache.
*
* @param string $key the launch id.
* @return array|null the launch data.
*/
public function getLaunchData(string $key): ?array {
global $SESSION;
if (isset($SESSION->enrol_lti_launch[$key])) {
return unserialize($SESSION->enrol_lti_launch[$key]);
}
return null;
}
/**
* Add launch data to the cache.
*
* @param string $key the launch id.
* @param array $jwtBody the launch data.
*/
public function cacheLaunchData(string $key, array $jwtBody): void {
global $SESSION;
$SESSION->enrol_lti_launch[$key] = serialize($jwtBody);
}
/**
* Cache the nonce.
*
* @param string $nonce the nonce.
* @param string $state the state.
*/
public function cacheNonce(string $nonce, string $state): void {
global $SESSION;
$SESSION->enrol_lti_launch_nonce[$nonce] = $state;
}
/**
* Check whether the cache contains the nonce.
*
* @param string $nonce the nonce
* @param string $state the state
* @return bool true if found, false otherwise.
*/
public function checkNonceIsValid(string $nonce, string $state): bool {
global $SESSION;
return isset($SESSION->enrol_lti_launch_nonce[$nonce]) && $SESSION->enrol_lti_launch_nonce[$nonce] == $state;
}
/**
* Delete all data from the session cache.
*/
public function purge() {
global $SESSION;
unset($SESSION->enrol_lti_launch);
}
/**
* Cache the access token.
*
* @param string $key the key
* @param string $accessToken the access token
*/
public function cacheAccessToken(string $key, string $accessToken): void {
global $SESSION;
$SESSION->enrol_lti_launch_token[$key] = $accessToken;
}
/**
* Get a cached access token.
*
* @param string $key the key to check.
* @return string|null the token string, or null if not found.
*/
public function getAccessToken(string $key): ?string {
global $SESSION;
return $SESSION->enrol_lti_launch_token[$key] ?? null;
}
/**
* Clear the access token from the cache.
*
* @param string $key the key to purge.
*/
public function clearAccessToken(string $key): void {
global $SESSION;
unset($SESSION->enrol_lti_launch_token[$key]);
}
}
@@ -0,0 +1,65 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\lib;
use core\session\utility\cookie_helper;
use Packback\Lti1p3\Interfaces\ICookie;
/**
* Cookie representation used by the lti1p3 library code.
*
* This implementation is a copy of the Packback ImsCookie implementation, a class previously included in the library
* but which is now deprecated there.
*
* @package enrol_lti
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lti_cookie implements ICookie {
public function getCookie(string $name): ?string {
if (isset($_COOKIE[$name])) {
return $_COOKIE[$name];
}
// Look for backup cookie if same site is not supported by the user's browser.
if (isset($_COOKIE['LEGACY_'.$name])) {
return $_COOKIE['LEGACY_'.$name];
}
return null;
}
public function setCookie(string $name, string $value, int $exp = 3600, array $options = []): void {
$cookieoptions = [
'expires' => time() + $exp,
];
// SameSite none and secure will be required for tools to work inside iframes.
$samesiteoptions = [
'samesite' => 'None',
'secure' => true,
];
setcookie($name, $value, array_merge($cookieoptions, $samesiteoptions, $options));
// Necessary, since partitioned can't be set via setcookie yet.
cookie_helper::add_attributes_to_cookie_response_header($name, ['Partitioned']);
// Set a second fallback cookie in the event that "SameSite" is not supported.
setcookie('LEGACY_'.$name, $value, array_merge($cookieoptions, $options));
}
}
@@ -0,0 +1,294 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\repository;
use enrol_lti\local\ltiadvantage\entity\application_registration;
/**
* Class application_registration_repository.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class application_registration_repository {
/** @var string $applicationregistrationtable the table containing application registrations. */
private $applicationregistrationtable = 'enrol_lti_app_registration';
/**
* Create an application_registration instance from a record.
*
* @param \stdClass $record the record.
* @return application_registration an application_registration instance.
*/
private function application_registration_from_record(\stdClass $record): application_registration {
if ($record->status == application_registration::REGISTRATION_STATUS_INCOMPLETE) {
$appreg = application_registration::create_draft(
$record->name,
$record->uniqueid,
$record->id
);
if (!empty($record->platformid)) {
$appreg->set_platformid(new \moodle_url($record->platformid));
}
if (!empty($record->clientid)) {
$appreg->set_clientid($record->clientid);
}
if (!empty($record->authenticationrequesturl)) {
$appreg->set_authenticationrequesturl(new \moodle_url($record->authenticationrequesturl));
}
if (!empty($record->jwksurl)) {
$appreg->set_jwksurl(new \moodle_url($record->jwksurl));
}
if (!empty($record->accesstokenurl)) {
$appreg->set_accesstokenurl(new \moodle_url($record->accesstokenurl));
}
} else if ($record->status == application_registration::REGISTRATION_STATUS_COMPLETE) {
$appreg = application_registration::create(
$record->name,
$record->uniqueid,
new \moodle_url($record->platformid),
$record->clientid,
new \moodle_url($record->authenticationrequesturl),
new \moodle_url($record->jwksurl),
new \moodle_url($record->accesstokenurl),
$record->id
);
}
return $appreg;
}
/**
* Get an array of application_registration instances from a set of records.
*
* @param \stdClass[] $records the array of records.
* @return array|application_registration[] the array of object instances.
*/
private function application_registrations_from_records(array $records): array {
if (empty($records)) {
return [];
}
return array_map(function($record) {
return $this->application_registration_from_record($record);
}, $records);
}
/**
* Convert the application_registration object into a stdClass for use with the data store.
*
* @param application_registration $appregistration the app registration.
* @return \stdClass the record.
*/
private function record_from_application_registration(application_registration $appregistration): \stdClass {
$appregistrationrecord = [
'name' => $appregistration->get_name(),
'uniqueid' => $appregistration->get_uniqueid(),
'status' => $appregistration->is_complete() ? application_registration::REGISTRATION_STATUS_COMPLETE
: application_registration::REGISTRATION_STATUS_INCOMPLETE
];
$platformid = $appregistration->get_platformid();
$clientid = $appregistration->get_clientid();
$authrequesturl = $appregistration->get_authenticationrequesturl();
$jwksurl = $appregistration->get_jwksurl();
$accesstokenurl = $appregistration->get_accesstokenurl();
$appregistrationrecord['platformid'] = !is_null($platformid) ? $platformid->out(false) : null;
$appregistrationrecord['clientid'] = $clientid;
$appregistrationrecord['authenticationrequesturl'] = !is_null($authrequesturl) ? $authrequesturl->out(false) : null;
$appregistrationrecord['jwksurl'] = !is_null($jwksurl) ? $jwksurl->out(false) : null;
$appregistrationrecord['accesstokenurl'] = !is_null($accesstokenurl) ? $accesstokenurl->out(false) : null;
if ($platformid && $clientid) {
$indexhash = $this->get_unique_index_hash($appregistration->get_platformid()->out(false),
$appregistration->get_clientid());
$appregistrationrecord['platformclienthash'] = $indexhash;
}
if ($platformid) {
$indexhash = $this->get_unique_index_hash($appregistration->get_platformid()->out(false),
$appregistration->get_uniqueid());
$appregistrationrecord['platformuniqueidhash'] = $indexhash;
}
if ($id = $appregistration->get_id()) {
$appregistrationrecord['id'] = $id;
}
return (object) $appregistrationrecord;
}
/**
* Gets a hash of the {platformid, clientid} tuple for use in indexing purposes.
*
* @param string $platformid the platformid of the registration.
* @param string $clientid the clientid of the registration
* @return string a SHA256 hash.
*/
private function get_unique_index_hash(string $platformid, string $clientid): string {
return hash('sha256', $platformid . ':' . $clientid);
}
/**
* Find a registration by id.
*
* @param int $id the id of the application registration.
* @return null|application_registration the registration object if found, otherwise null.
*/
public function find(int $id): ?application_registration {
global $DB;
try {
$record = $DB->get_record($this->applicationregistrationtable, ['id' => $id], '*', MUST_EXIST);
return $this->application_registration_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Get all app registrations in the repository.
*
* @return application_registration[] the array of application registration instances.
*/
public function find_all(): array {
global $DB;
return $this->application_registrations_from_records($DB->get_records($this->applicationregistrationtable));
}
/**
* Find a registration by its unique {platformid, uniqueid} tuple.
*
* @param string $platformid the url of the platform (the issuer).
* @param string $uniqueid the locally uniqueid of the tool registration.
* @return application_registration|null application registration instance if found, else null.
*/
public function find_by_platform_uniqueid(string $platformid, string $uniqueid): ?application_registration {
global $DB;
try {
$indexhash = $this->get_unique_index_hash($platformid, $uniqueid);
$record = $DB->get_record($this->applicationregistrationtable, ['platformuniqueidhash' => $indexhash], '*',
MUST_EXIST);
return $this->application_registration_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Find a registration by its uniqueid.
*
* @param string $uniqueid the uniqueid identifying the registration.
* @return application_registration|null application_registration instance if found, else null.
*/
public function find_by_uniqueid(string $uniqueid): ?application_registration {
global $DB;
try {
$record = $DB->get_record($this->applicationregistrationtable, ['uniqueid' => $uniqueid], '*', MUST_EXIST);
return $this->application_registration_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Find a registration by its unique {platformid, clientid} tuple.
*
* @param string $platformid the url of the platform (the issuer).
* @param string $clientid the client_id of the tool registration on the platform.
* @return application_registration|null application registration instance if found, else null.
*/
public function find_by_platform(string $platformid, string $clientid): ?application_registration {
global $DB;
try {
$indexhash = $this->get_unique_index_hash($platformid, $clientid);
$record = $DB->get_record($this->applicationregistrationtable, ['platformclienthash' => $indexhash], '*',
MUST_EXIST);
return $this->application_registration_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Find an application_registration corresponding to the local id of a given tool deployment.
*
* @param int $deploymentid the local id of the tool deployment object.
* @return application_registration|null the application_registration instance or null if not found.
*/
public function find_by_deployment(int $deploymentid): ?application_registration {
global $DB;
try {
$sql = "SELECT a.id, a.name, a.platformid, a.clientid, a.authenticationrequesturl, a.jwksurl,
a.accesstokenurl, a.uniqueid, a.status, a.timecreated, a.timemodified
FROM {enrol_lti_app_registration} a
JOIN {enrol_lti_deployment} d
ON (d.platformid = a.id)
WHERE d.id = :id";
$record = $DB->get_record_sql($sql, ['id' => $deploymentid], MUST_EXIST);
return $this->application_registration_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Save an application_registration instance to the store.
*
* @param application_registration $appregistration the application registration instance.
* @return application_registration the saved application registration instance.
*/
public function save(application_registration $appregistration): application_registration {
global $DB;
$id = $appregistration->get_id();
$exists = $id ? $this->exists($id) : false;
$record = $this->record_from_application_registration($appregistration);
$timenow = time();
if ($exists) {
$record->timemodified = $timenow;
$DB->update_record($this->applicationregistrationtable, $record);
} else {
$record->timecreated = $record->timemodified = $timenow;
$appregid = $DB->insert_record($this->applicationregistrationtable, $record);
$record->id = $appregid;
}
return $this->application_registration_from_record($record);
}
/**
* Report whether an application_registration with id $id exists or not.
*
* @param int $appregid the id of the application_registration
* @return bool true if the object exists, false otherwise.
*/
public function exists(int $appregid): bool {
global $DB;
return $DB->record_exists($this->applicationregistrationtable, ['id' => $appregid]);
}
/**
* Delete the application_registration identified by id.
*
* @param int $id the id of the object to delete.
*/
public function delete(int $id): void {
global $DB;
$DB->delete_records($this->applicationregistrationtable, ['id' => $id]);
}
}
@@ -0,0 +1,158 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\repository;
use enrol_lti\local\ltiadvantage\entity\context;
/**
* Class context_repository.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class context_repository {
/** @var string the name of the table storing object data. */
private $contexttable = 'enrol_lti_context';
/**
* Generate a context instance from a record.
*
* @param \stdClass $record the record.
* @return context the context instance.
*/
private function context_from_record(\stdClass $record): context {
$context = context::create(
$record->ltideploymentid,
$record->contextid,
json_decode($record->type),
$record->id
);
return $context;
}
/**
* Generate a record from a context instance.
*
* @param context $context the context instance.
* @return \stdClass the resulting record.
*/
private function record_from_context(context $context): \stdClass {
$record = [
'contextid' => $context->get_contextid(),
'ltideploymentid' => $context->get_deploymentid(),
'type' => json_encode($context->get_types()),
];
if ($id = $context->get_id()) {
$record['id'] = $id;
}
return (object) $record;
}
/**
* Save the context to the store.
*
* @param context $context the context to save.
* @return context the saved context instance.
*/
public function save(context $context): context {
global $DB;
$id = $context->get_id();
$exists = $id ? $this->exists($id) : false;
$record = $this->record_from_context($context);
$timenow = time();
if ($exists) {
$record->timemodified = $timenow;
$DB->update_record($this->contexttable, $record);
} else {
$record->timecreated = $record->timemodified = $timenow;
$id = $DB->insert_record($this->contexttable, $record);
$record->id = $id;
}
return $this->context_from_record($record);
}
/**
* Find a context by id.
*
* @param int $id the id of the instance.
* @return context|null the context, if found, else null.
*/
public function find(int $id): ?context {
global $DB;
try {
$record = $DB->get_record($this->contexttable, ['id' => $id], '*', MUST_EXIST);
return $this->context_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Find a context by it's platform-issued context id string.
*
* @param string $contextid the id of the context on the platform.
* @param int $deploymentid the id of the local deployment instance in which the contextid is unique.
* @return context|null the context instance, if found, else null.
*/
public function find_by_contextid(string $contextid, int $deploymentid): ?context {
global $DB;
try {
$record = $DB->get_record($this->contexttable,
['contextid' => $contextid, 'ltideploymentid' => $deploymentid], '*', MUST_EXIST);
return $this->context_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Check whether the context identified by 'id' exists in the store.
*
* @param int $id the id of the instance to check.
* @return bool true if found, false otherwise.
*/
public function exists(int $id): bool {
global $DB;
return $DB->record_exists($this->contexttable, ['id' => $id]);
}
/**
* Delete the context identified by 'id' from the store.
*
* @param int $id the id of context to delete.
*/
public function delete(int $id): void {
global $DB;
$DB->delete_records($this->contexttable, ['id' => $id]);
}
/**
* Delete all contexts under a given deployment.
*
* @param int $deploymentid the id of the local deployment instance.
*/
public function delete_by_deployment(int $deploymentid): void {
global $DB;
$DB->delete_records($this->contexttable, ['ltideploymentid' => $deploymentid]);
}
}
@@ -0,0 +1,207 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\repository;
use enrol_lti\local\ltiadvantage\entity\deployment;
/**
* The deployment_repository class.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class deployment_repository {
/** @var string $deploymenttable the name of table containing deployments. */
private $deploymenttable = 'enrol_lti_deployment';
/**
* Create a valid record from a deployment instance.
*
* @param deployment $deployment the deployment.
* @return \stdClass a compatible record.
*/
private function record_from_deployment(deployment $deployment): \stdClass {
$record = (object) [
'name' => $deployment->get_deploymentname(),
'deploymentid' => $deployment->get_deploymentid(),
'platformid' => $deployment->get_registrationid(),
'legacyconsumerkey' => $deployment->get_legacy_consumer_key()
];
if ($id = $deployment->get_id()) {
$record->id = $id;
}
return $record;
}
/**
* Create a list of deployments based on a list of records.
*
* @param array $records an array of deployment records.
* @return deployment[]
*/
private function deployments_from_records(array $records): array {
if (empty($records)) {
return [];
}
return array_map(function($record) {
return $this->deployment_from_record($record);
}, $records);
}
/**
* Create a valid deployment from a record.
*
* @param \stdClass $record the record.
* @return deployment the deployment instance.
*/
private function deployment_from_record(\stdClass $record): deployment {
$deployment = deployment::create(
$record->platformid,
$record->deploymentid,
$record->name,
$record->id,
$record->legacyconsumerkey
);
return $deployment;
}
/**
* Save a deployment to the store.
*
* @param deployment $deployment the deployment instance to save.
* @return deployment the saved deployment instance.
*/
public function save(deployment $deployment): deployment {
global $DB;
$id = $deployment->get_id();
$exists = $id ? $this->exists($id) : false;
$record = $this->record_from_deployment($deployment);
$timenow = time();
if ($exists) {
$record->timemodified = $timenow;
$DB->update_record($this->deploymenttable, $record);
} else {
$record->timecreated = $record->timemodified = $timenow;
$id = $DB->insert_record($this->deploymenttable, $record);
$record->id = $id;
}
return $this->deployment_from_record($record);
}
/**
* Find and return a deployment, by id.
*
* @param int $id the id of the deployment to find.
* @return deployment|null
*/
public function find(int $id): ?deployment {
global $DB;
try {
$record = $DB->get_record($this->deploymenttable, ['id' => $id], '*', MUST_EXIST);
return $this->deployment_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Determine whether a deployment exists in the repository.
*
* @param int $id the identifier of the deployment
* @return bool true if the deployment exists, false otherwise.
*/
public function exists(int $id): bool {
global $DB;
return $DB->record_exists($this->deploymenttable, ['id' => $id]);
}
/**
* Delete a deployment from the store.
*
* @param int $id the id of the deployment object to remove.
*/
public function delete(int $id): void {
global $DB;
$DB->delete_records($this->deploymenttable, ['id' => $id]);
}
/**
* Delete all deployments for the given registration.
*
* @param int $registrationid the registration id.
*/
public function delete_by_registration(int $registrationid): void {
global $DB;
$DB->delete_records($this->deploymenttable, ['platformid' => $registrationid]);
}
/**
* Return a count of how many deployments exists for a given application_registration.
*
* @param int $registrationid the id of the application_registration instance.
* @return int the number of deployments found.
*/
public function count_by_registration(int $registrationid): int {
global $DB;
return $DB->count_records($this->deploymenttable, ['platformid' => $registrationid]);
}
/**
* Get a deployment based on its deploymentid and a for a given application registration id.
*
* @param int $registrationid the id of the application_registration to which the deployment belongs.
* @param string $deploymentid the deploymentid of the deployment, as set by the platform.
* @return deployment|null deployment if found, otherwise null.
*/
public function find_by_registration(int $registrationid, string $deploymentid): ?deployment {
global $DB;
try {
$sql = "SELECT eld.id, eld.name, eld.deploymentid, eld.platformid, eld.legacyconsumerkey
FROM {".$this->deploymenttable."} eld
JOIN {enrol_lti_app_registration} elar
ON (eld.platformid = elar.id)
WHERE elar.id = :registrationid
AND eld.deploymentid = :deploymentid";
$params = ['registrationid' => $registrationid, 'deploymentid' => $deploymentid];
$record = $DB->get_record_sql($sql, $params, MUST_EXIST);
return $this->deployment_from_record($record);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Get all deployments for a given application registration id.
*
* @param int $registrationid the id of the application_registration to which the deployment belongs.
* @return deployment[]|null deployments if found, otherwise null.
*/
public function find_all_by_registration(int $registrationid): ?array {
global $DB;
$sql = "SELECT eld.id, eld.name, eld.deploymentid, eld.platformid, eld.legacyconsumerkey
FROM {".$this->deploymenttable."} eld
JOIN {enrol_lti_app_registration} elar
ON (eld.platformid = elar.id)
WHERE elar.id = :registrationid";
$records = $DB->get_records_sql($sql, ['registrationid' => $registrationid]);
return $this->deployments_from_records($records);
}
}
@@ -0,0 +1,47 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\repository;
/**
* The legacy_consumer_repository class, instances of which are responsible for querying LTI 1.1/2.0 consumer info.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class legacy_consumer_repository {
/**
* Get a list of all shared secrets which a given LTI 1.1/2.0 consumer is associated with.
*
* A single consumer key may be used across several tool definitions, with different secrets, thus permitting a
* one:many relationship between consumer and secret.
* @param string $consumerkey the key identifying the consumer.
* @return string[] an array of secrets corresponding to the consumer key.
*/
public function get_consumer_secrets(string $consumerkey): array {
global $DB;
$sql = "SELECT t.id, t.secret
FROM {enrol_lti_lti2_consumer} c
JOIN {enrol_lti_tool_consumer_map} cm
ON (c.id = cm.consumerid)
JOIN {enrol_lti_tools} t
ON (t.id = cm.toolid)
WHERE c.consumerkey256 = :consumerkey";
return array_unique(array_column($DB->get_records_sql($sql, ['consumerkey' => $consumerkey]), 'secret'));
}
}
@@ -0,0 +1,210 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\repository;
use core_availability\info_module;
use enrol_lti\local\ltiadvantage\viewobject\published_resource;
/**
* Class published_resource_repository for fetching the published_resource instances from the store.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class published_resource_repository {
/**
* Convert a list of stdClasses to a list of published_resource instances.
*
* @param array $records the records.
* @return array the array of published_resource instances.
*/
private function published_resources_from_records(array $records): array {
$publishedresources = [];
foreach ($records as $record) {
$publishedresource = new published_resource(
$record->name,
$record->coursefullname,
$record->courseid,
$record->contextid,
$record->id,
$record->uuid,
$record->supportsgrades,
$record->grademax ?? null,
$record->iscourse,
);
$publishedresources[] = $publishedresource;
}
return $publishedresources;
}
/**
* Given a list of published resources, return a list of those which are available to the provided user.
*
* @param array $resources the array of records representing published resources.
* @param int $userid the Moodle user id to check.
* @return array an array of stdClasses containing data about resources which are available to the current user.
*/
private function get_available_resources_from_records(array $resources, int $userid): array {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
require_once($CFG->libdir . '/moodlelib.php');
$availableresources = [];
foreach ($resources as $resource) {
if ($resource->contextlevel == CONTEXT_COURSE) {
// Shared item is a course.
if (!can_access_course(get_course($resource->courseid), $userid)) {
continue;
}
$resource->name = format_string($resource->coursefullname, true, ['context' => $resource->contextid]);
$resource->coursefullname = $resource->name;
$resource->iscourse = true;
$resource->supportsgrades = true;
$coursegradeitem = \grade_item::fetch_course_item($resource->courseid);
$resource->grademax = $coursegradeitem->grademax;
$availableresources[] = $resource;
} else if ($resource->contextlevel = CONTEXT_MODULE) {
// Shared item is a module.
$resource->coursefullname = format_string($resource->coursefullname, true,
['context' => $resource->contextid]);
$mods = get_fast_modinfo($resource->courseid, $userid)->get_cms();
foreach ($mods as $mod) {
if ($mod->context->id == $resource->contextid) {
if (info_module::is_user_visible($mod->id, $userid, true)) {
$resource->iscourse = false;
$resource->name = $mod->name;
$resource->supportsgrades = false;
$resource->grademax = null;
// Only activities having a single grade item of GRADE_TYPE_VALUE are eligible for declarative binding.
if (plugin_supports('mod', $mod->modname, FEATURE_GRADE_HAS_GRADE)) {
$gradinginfo = grade_get_grades($resource->courseid, 'mod', $mod->modname, $mod->instance);
if (count($gradinginfo->items) == 1) {
$gradeitem = \grade_item::fetch([
'courseid' => $resource->courseid,
'itemtype' => 'mod',
'itemmodule' => $mod->modname,
'iteminstance' => $mod->instance
]);
if ($gradeitem && $gradeitem->gradetype == GRADE_TYPE_VALUE) {
$resource->supportsgrades = true;
$resource->grademax = (int)$gradinginfo->items[0]->grademax;
}
}
}
$availableresources[] = $resource;
}
}
}
}
}
return $availableresources;
}
/**
* Find all published resources which are visible to the given user.
*
* @param int $userid the id of the user to check.
* @return published_resource[] an array of published_resource view objects instances.
*/
public function find_all_for_user(int $userid): array {
global $DB, $CFG;
require_once($CFG->libdir . '/accesslib.php');
require_once($CFG->libdir . '/enrollib.php');
require_once($CFG->libdir . '/moodlelib.php');
require_once($CFG->libdir . '/modinfolib.php');
require_once($CFG->libdir . '/weblib.php');
[$insql, $inparams] = $DB->get_in_or_equal(['LTI-1p3'], SQL_PARAMS_NAMED);
$sql = "SELECT elt.id, elt.uuid, elt.enrolid, elt.contextid, elt.institution, elt.lang, elt.timezone,
elt.maxenrolled, elt.maildisplay, elt.city, elt.country, elt.gradesync, elt.gradesynccompletion,
elt.membersync, elt.membersyncmode, elt.roleinstructor, elt.rolelearner, e.name AS enrolname,
e.courseid, ctx.contextlevel, c.fullname AS coursefullname
FROM {enrol} e
JOIN {enrol_lti_tools} elt
ON (e.id = elt.enrolid and e.status = :enrolstatusenabled)
JOIN {course} c
ON (c.id = e.courseid)
JOIN {context} ctx
ON (ctx.id = elt.contextid)
WHERE elt.ltiversion $insql
ORDER BY courseid";
$params = array_merge($inparams, ['enrolstatusenabled' => ENROL_INSTANCE_ENABLED]);
$resources = $DB->get_records_sql($sql, $params);
// Only users who have the ability to publish content should see published content.
$resources = array_filter($resources, function($resource) use ($userid) {
return has_capability('enrol/lti:config', \context_course::instance($resource->courseid), $userid);
});
// Make sure the user can access each course or module, excluding those which are inaccessible from the return.
$availableresources = $this->get_available_resources_from_records($resources, $userid);
return $this->published_resources_from_records($availableresources);
}
/**
* Find all published_resource instances matching the supplied ids for the current user.
*
* @param array $ids the array containing object ids to lookup
* @param int $userid the id of the user to check
* @return array an array of published_resource instances which are available to the user.
*/
public function find_all_by_ids_for_user(array $ids, int $userid): array {
global $DB, $CFG;
if (empty($ids)) {
return [];
}
require_once($CFG->libdir . '/accesslib.php');
require_once($CFG->libdir . '/enrollib.php');
require_once($CFG->libdir . '/moodlelib.php');
require_once($CFG->libdir . '/modinfolib.php');
require_once($CFG->libdir . '/weblib.php');
[$insql, $inparams] = $DB->get_in_or_equal(['LTI-1p3'], SQL_PARAMS_NAMED);
[$idsinsql, $idsinparams] = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
$sql = "SELECT elt.id, elt.uuid, elt.enrolid, elt.contextid, elt.institution, elt.lang, elt.timezone,
elt.maxenrolled, elt.maildisplay, elt.city, elt.country, elt.gradesync, elt.gradesynccompletion,
elt.membersync, elt.membersyncmode, elt.roleinstructor, elt.rolelearner, e.name AS enrolname,
e.courseid, ctx.contextlevel, c.fullname AS coursefullname
FROM {enrol} e
JOIN {enrol_lti_tools} elt
ON (e.id = elt.enrolid and e.status = :enrolstatusenabled)
JOIN {course} c
ON (c.id = e.courseid)
JOIN {context} ctx
ON (ctx.id = elt.contextid)
WHERE elt.ltiversion $insql
AND elt.id $idsinsql
ORDER BY courseid";
$params = array_merge($inparams, $idsinparams, ['enrolstatusenabled' => ENROL_INSTANCE_ENABLED]);
$resources = $DB->get_records_sql($sql, $params);
// Make sure the user can access each course or module, excluding those which are inaccessible from the return.
$availableresources = $this->get_available_resources_from_records($resources, $userid);
return $this->published_resources_from_records($availableresources);
}
}
@@ -0,0 +1,294 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\repository;
use enrol_lti\local\ltiadvantage\entity\deployment;
use enrol_lti\local\ltiadvantage\entity\resource_link;
/**
* Class resource_link_repository.
*
* This class encapsulates persistence logic for \enrol_lti\local\entity\resource_link type objects.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class resource_link_repository {
/** @var string the name of the table to which the entity will be persisted */
private $table = 'enrol_lti_resource_link';
/** @var string the name of the table to which user-entity mappings will have been persisted. */
private $userresourcelinkmaptable = 'enrol_lti_user_resource_link';
/**
* Convert a record into an object and return it.
*
* @param \stdClass $record the record from the store.
* @return resource_link a resource_link object.
*/
private function resource_link_from_record(\stdClass $record): resource_link {
$resourcelink = resource_link::create(
$record->resourcelinkid,
$record->ltideploymentid,
$record->resourceid,
$record->lticontextid,
$record->id
);
if ($record->lineitemsservice || $record->lineitemservice) {
$scopes = [];
if ($record->lineitemscope) {
$lineitemscopes = json_decode($record->lineitemscope);
foreach ($lineitemscopes as $lineitemscope) {
$scopes[] = $lineitemscope;
}
}
if ($record->resultscope) {
$scopes[] = $record->resultscope;
}
if ($record->scorescope) {
$scopes[] = $record->scorescope;
}
$resourcelink->add_grade_service(
$record->lineitemsservice ? new \moodle_url($record->lineitemsservice) : null,
$record->lineitemservice ? new \moodle_url($record->lineitemservice) : null,
$scopes
);
}
if ($record->contextmembershipsurl) {
$resourcelink->add_names_and_roles_service(
new \moodle_url($record->contextmembershipsurl),
json_decode($record->nrpsserviceversions)
);
}
return $resourcelink;
}
/**
* Get a list of resource_link objects from a list of records.
*
* @param array $records the list of records to transform.
* @return array the array of resource_link instances.
*/
private function resource_links_from_records(array $records): array {
$resourcelinks = [];
foreach ($records as $record) {
$resourcelinks[] = $this->resource_link_from_record($record);
}
return $resourcelinks;
}
/**
* Get a stdClass object ready for persisting, based on the supplied resource_link object.
*
* @param resource_link $resourcelink the resource link instance.
* @return \stdClass the record.
*/
private function record_from_resource_link(resource_link $resourcelink): \stdClass {
$gradeservice = $resourcelink->get_grade_service();
$nrpservice = $resourcelink->get_names_and_roles_service();
$record = [
'id' => $resourcelink->get_id(),
'resourcelinkid' => $resourcelink->get_resourcelinkid(),
'ltideploymentid' => $resourcelink->get_deploymentid(),
'resourceid' => $resourcelink->get_resourceid(),
'lticontextid' => $resourcelink->get_contextid(),
'lineitemsservice' => null,
'lineitemservice' => null,
'lineitemscope' => null,
'resultscope' => $gradeservice ? $gradeservice->get_resultscope() : null,
'scorescope' => $gradeservice ? $gradeservice->get_scorescope() : null,
'contextmembershipsurl' => $nrpservice ? $nrpservice->get_context_memberships_url()->out(false) : null,
'nrpsserviceversions' => $nrpservice ? json_encode($nrpservice->get_service_versions()) : null
];
if ($gradeservice && ($lineitemsurl = $gradeservice->get_lineitemsurl())) {
$record['lineitemsservice'] = $lineitemsurl->out(false);
}
if ($gradeservice && ($lineitemurl = $gradeservice->get_lineitemurl())) {
$record['lineitemservice'] = $lineitemurl->out(false);
}
if ($gradeservice && ($lineitemscopes = $gradeservice->get_lineitemscope())) {
$record['lineitemscope'] = json_encode($lineitemscopes);
}
return (object) $record;
}
/**
* Save a resource link instance in the store.
*
* @param resource_link $resourcelink the object to save.
* @return resource_link the saved object.
*/
public function save(resource_link $resourcelink): resource_link {
global $DB;
$id = $resourcelink->get_id();
$exists = $id ? $this->exists($id) : false;
if ($id && !$exists) {
throw new \coding_exception("Cannot save resource_link with id '{$id}'. The record does not exist.");
}
$record = $this->record_from_resource_link($resourcelink);
$timenow = time();
if ($exists) {
$record->timemodified = $timenow;
$DB->update_record($this->table, $record);
} else {
$record->timecreated = $record->timemodified = $timenow;
$id = $DB->insert_record($this->table, $record);
$record->id = $id;
}
return $this->resource_link_from_record($record);
}
/**
* Find and return a resource_link by id.
*
* @param int $id the id of the resource_link object.
* @return resource_link|null the resource_link object, or null if the object cannot be found.
*/
public function find(int $id): ?resource_link {
global $DB;
try {
$record = $DB->get_record($this->table, ['id' => $id], '*', MUST_EXIST);
return $this->resource_link_from_record($record);
} catch (\dml_missing_record_exception $ex) {
return null;
}
}
/**
* Get a resource by id, within a given tool deployment.
*
* @param deployment $deployment the deployment instance.
* @param string $resourcelinkid the resourcelinkid from the platform.
* @return resource_link|null the resource link instance, or null if not found.
*/
public function find_by_deployment(deployment $deployment, string $resourcelinkid): ?resource_link {
global $DB;
try {
$record = $DB->get_record($this->table, ['ltideploymentid' => $deployment->get_id(),
'resourcelinkid' => $resourcelinkid], '*', MUST_EXIST);
return $this->resource_link_from_record($record);
} catch (\dml_missing_record_exception $ex) {
return null;
}
}
/**
* Find resource_link objects based on the resource and a given launching user.
*
* @param int $resourceid the local id of the resource (enrol_lti_tools id)
* @param int $userid the local id of the enrol_lti\local\ltiadvantage\user object
* @return array an array of resource_links
*/
public function find_by_resource_and_user(int $resourceid, int $userid): array {
global $DB;
$sql = "SELECT r.id, r.resourcelinkid, r.resourceid, r.ltideploymentid, r.lticontextid, r.lineitemsservice,
r.lineitemservice, r.lineitemscope, r.resultscope, r.scorescope, r.contextmembershipsurl,
r.nrpsserviceversions, r.timecreated, r.timemodified
FROM {enrol_lti_resource_link} r
JOIN {enrol_lti_user_resource_link} ur
ON (r.id = ur.resourcelinkid)
WHERE ur.ltiuserid = :ltiuserid
AND r.resourceid = :resourceid";
$records = $DB->get_records_sql($sql, ['ltiuserid' => $userid, 'resourceid' => $resourceid]);
return $this->resource_links_from_records($records);
}
/**
* Gets all mapped resource links for a given resource.
*
* @param int $resourceid the local id of the shared resource.
* @return array the array of resource_link instances.
*/
public function find_by_resource(int $resourceid): array {
global $DB;
$records = $DB->get_records($this->table, ['resourceid' => $resourceid]);
return $this->resource_links_from_records($records);
}
/**
* Check whether or not the given resource_link object exists.
*
* @param int $id the unique id the resource_link.
* @return bool true if found, false otherwise.
*/
public function exists(int $id): bool {
global $DB;
return $DB->record_exists($this->table, ['id' => $id]);
}
/**
* Delete a resource_link based on id.
*
* @param int $id the id of the resource_link to remove.
*/
public function delete(int $id) {
global $DB;
// First remove all enrol_lti_user_resource_link mappings.
$DB->delete_records($this->userresourcelinkmaptable, ['resourcelinkid' => $id]);
// And the resource_link itself.
$DB->delete_records($this->table, ['id' => $id]);
}
/**
* Delete all resource links for a given deployment, as well as any mappings between users and the respective links.
*
* @param int $deploymentid the id of the deployment instance.
*/
public function delete_by_deployment(int $deploymentid): void {
global $DB;
// First remove all enrol_lti_user_resource_link mappings.
$DB->delete_records_select(
$this->userresourcelinkmaptable,
"resourcelinkid IN (SELECT id FROM {{$this->table}} WHERE ltideploymentid = :ltideploymentid)",
['ltideploymentid' => $deploymentid]
);
// And remove the resource_link entries themselves.
$DB->delete_records($this->table, ['ltideploymentid' => $deploymentid]);
}
/**
* Delete all resource_link instances referring to the resource identified by $resourceid.
*
* @param int $resourceid the id of the published resource.
*/
public function delete_by_resource(int $resourceid) {
global $DB;
// First remove all enrol_lti_user_resource_link mappings.
$DB->delete_records_select(
$this->userresourcelinkmaptable,
"resourcelinkid IN (SELECT id FROM {{$this->table}} WHERE resourceid = :resourceid)",
['resourceid' => $resourceid]
);
// And remove the resource_link entries themselves.
$DB->delete_records($this->table, ['resourceid' => $resourceid]);
}
}
@@ -0,0 +1,353 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\repository;
use enrol_lti\local\ltiadvantage\entity\user;
/**
* Class user_repository.
*
* This class encapsulates persistence logic for \enrol_lti\local\entity\user type objects.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_repository {
/** @var string $ltiuserstable the name of the table to which the entity will be persisted.*/
private $ltiuserstable = 'enrol_lti_users';
/** @var string $userresourcelinkidtable the name of the join table mapping users to resource links.*/
private $userresourcelinkidtable = 'enrol_lti_user_resource_link';
/**
* Convert a record into a user object and return it.
*
* @param \stdClass $userrecord the raw data from relevant tables required to instantiate a user.
* @return user a user object.
*/
private function user_from_record(\stdClass $userrecord): user {
return user::create(
$userrecord->toolid,
$userrecord->localid,
$userrecord->ltideploymentid,
$userrecord->sourceid,
$userrecord->lang,
$userrecord->timezone,
$userrecord->city,
$userrecord->country,
$userrecord->institution,
$userrecord->maildisplay,
$userrecord->lastgrade,
$userrecord->lastaccess,
$userrecord->resourcelinkid ?? null,
(int) $userrecord->id
);
}
/**
* Create a list of user instances from a list of records.
*
* @param array $records the array of records.
* @return array of user instances.
*/
private function users_from_records(array $records): array {
$users = [];
foreach ($records as $record) {
$users[] = $this->user_from_record($record);
}
return $users;
}
/**
* Get a stdClass object ready for persisting, based on the supplied user object.
*
* @param user $user the user instance.
* @return \stdClass the record.
*/
private function user_record_from_user(user $user): \stdClass {
return (object) [
'id' => $user->get_localid(),
'city' => $user->get_city(),
'country' => $user->get_country(),
'institution' => $user->get_institution(),
'timezone' => $user->get_timezone(),
'maildisplay' => $user->get_maildisplay(),
'lang' => $user->get_lang()
];
}
/**
* Create the corresponding enrol_lti_user record from a user instance.
*
* @param user $user the user instance.
* @return \stdClass the record.
*/
private function lti_user_record_from_user(user $user): \stdClass {
$record = [
'toolid' => $user->get_resourceid(),
'ltideploymentid' => $user->get_deploymentid(),
'sourceid' => $user->get_sourceid(),
'lastgrade' => $user->get_lastgrade(),
'lastaccess' => $user->get_lastaccess(),
];
if ($user->get_id()) {
$record['id'] = $user->get_id();
}
return (object) $record;
}
/**
* Helper to validate user:tool uniqueness across a deployment.
*
* The DB cannot be relied on to do this uniqueness check, since the table is shared by LTI 1.1/2.0 data.
*
* @param user $user the user instance.
* @return bool true if found, false otherwise.
*/
private function user_exists_for_tool(user $user): bool {
// Lack of an id doesn't preclude the object from existence in the store. It may be stale, without an id.
// The user can still be found by checking their lti advantage user creds and correlating that to the relevant
// lti_user entry (where tool matches the user object's resource).
global $DB;
$uniquesql = "SELECT lu.id
FROM {{$this->ltiuserstable}} lu
WHERE lu.toolid = :toolid
AND lu.userid = :userid";
$params = ['toolid' => $user->get_resourceid(), 'userid' => $user->get_localid()];
return $DB->record_exists_sql($uniquesql, $params);
}
/**
* Save a user instance in the store.
*
* @param user $user the object to save.
* @return user the saved object.
*/
public function save(user $user): user {
global $DB;
$id = $user->get_id();
$exists = !is_null($id) && $this->exists($id);
if ($id && !$exists) {
throw new \coding_exception("Cannot save lti user with id '{$id}'. The record does not exist.");
}
$userrecord = $this->user_record_from_user($user);
$ltiuserrecord = $this->lti_user_record_from_user($user);
$timenow = time();
global $CFG;
require_once($CFG->dirroot . '/user/lib.php');
if ($exists) {
$ltiuser = $DB->get_record($this->ltiuserstable, ['id' => $ltiuserrecord->id]);
$userid = $ltiuser->userid;
// Warn about localid vs ltiuser->userid mismatches here. Callers shouldn't be able to force updates using
// localid. Only new user associations can be created that way.
if (!empty($userrecord->id) && $userid != $userrecord->id) {
throw new \coding_exception("Cannot update user mapping. LTI user '{$ltiuser->id}' is already mapped " .
"to user '{$ltiuser->userid}' and can't be associated with another user '{$userrecord->id}'.");
}
// Only update the Moodle user record if something has changed.
$rawuser = \core_user::get_user($userrecord->id);
$userfieldstocompare = array_intersect_key(
(array) $rawuser,
(array) $userrecord
);
if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) {
\user_update_user($userrecord);
}
unset($userrecord->id);
$ltiuserrecord->timemodified = $timenow;
$DB->update_record($this->ltiuserstable, $ltiuserrecord);
} else {
// Validate uniqueness of the lti user, in the case of a stale object coming in to be saved.
if ($this->user_exists_for_tool($user)) {
throw new \coding_exception("Cannot create duplicate LTI user '{$user->get_localid()}' for resource " .
"'{$user->get_resourceid()}'.");
}
// Only update the Moodle user record if something has changed.
$userid = $userrecord->id;
$rawuser = \core_user::get_user($userid);
$userfieldstocompare = array_intersect_key(
(array) $rawuser,
(array) $userrecord
);
if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) {
\user_update_user($userrecord);
}
unset($userrecord->id);
// Create the lti_user record, holding details that have a lifespan equal to that of the enrolment instance.
$ltiuserrecord->timecreated = $ltiuserrecord->timemodified = $timenow;
$ltiuserrecord->userid = $userid;
$ltiuserrecord->id = $DB->insert_record($this->ltiuserstable, $ltiuserrecord);
}
// If the user was created via a resource_link, create that association.
if ($reslinkid = $user->get_resourcelinkid()) {
$resourcelinkmap = ['ltiuserid' => $ltiuserrecord->id, 'resourcelinkid' => $reslinkid];
if (!$DB->record_exists($this->userresourcelinkidtable, $resourcelinkmap)) {
$DB->insert_record($this->userresourcelinkidtable, $resourcelinkmap);
}
}
$resourcelinkmap = $resourcelinkmap ?? [];
// Transform the data into something that looks like a read and can be processed by user_from_record.
$record = (object) array_merge(
(array) $userrecord,
(array) $ltiuserrecord,
$resourcelinkmap,
['localid' => $userid]
);
return $this->user_from_record($record);
}
/**
* Find and return a user by id.
*
* @param int $id the id of the user object.
* @return user|null the user object, or null if the object cannot be found.
*/
public function find(int $id): ?user {
global $DB;
try {
$sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country,
u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade,
lu.lastaccess, lu.ltideploymentid
FROM {{$this->ltiuserstable}} lu
JOIN {user} u
ON (u.id = lu.userid)
WHERE lu.id = :id
AND lu.ltideploymentid IS NOT NULL";
$record = $DB->get_record_sql($sql, ['id' => $id], MUST_EXIST);
return $this->user_from_record($record);
} catch (\dml_missing_record_exception $ex) {
return null;
}
}
/**
* Find an lti user instance by resource.
*
* @param int $userid the id of the moodle user to look for.
* @param int $resourceid the id of the published resource.
* @return user|null the lti user instance, or null if not found.
*/
public function find_single_user_by_resource(int $userid, int $resourceid): ?user {
global $DB;
try {
// Find the lti advantage user record.
$sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country,
u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade,
lu.lastaccess, lu.ltideploymentid
FROM {{$this->ltiuserstable}} lu
JOIN {user} u
ON (u.id = lu.userid)
WHERE lu.userid = :userid
AND lu.toolid = :resourceid
AND lu.ltideploymentid IS NOT NULL";
$params = ['userid' => $userid, 'resourceid' => $resourceid];
$record = $DB->get_record_sql($sql, $params, MUST_EXIST);
return $this->user_from_record($record);
} catch (\dml_missing_record_exception $ex) {
return null;
}
}
/**
* Find all users for a particular shared resource.
*
* @param int $resourceid the id of the shared resource.
* @return array the array of users, empty if none were found.
*/
public function find_by_resource(int $resourceid): array {
global $DB;
$sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country,
u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade,
lu.lastaccess, lu.ltideploymentid
FROM {{$this->ltiuserstable}} lu
JOIN {user} u
ON (u.id = lu.userid)
WHERE lu.toolid = :resourceid
AND lu.ltideploymentid IS NOT NULL
ORDER BY lu.lastaccess DESC";
$records = $DB->get_records_sql($sql, ['resourceid' => $resourceid]);
return $this->users_from_records($records);
}
/**
* Get a list of users associated with the given resource link.
*
* @param int $resourcelinkid the id of the resource_link instance with which the users are associated.
* @return array the array of users, empty if none were found.
*/
public function find_by_resource_link(int $resourcelinkid) {
global $DB;
$sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country,
u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade,
lu.lastaccess, lu.ltideploymentid
FROM {{$this->ltiuserstable}} lu
JOIN {user} u
ON (u.id = lu.userid)
JOIN {{$this->userresourcelinkidtable}} url
ON (url.ltiuserid = lu.id)
WHERE url.resourcelinkid = :resourcelinkid
ORDER BY lu.lastaccess DESC";
$records = $DB->get_records_sql($sql, ['resourcelinkid' => $resourcelinkid]);
return $this->users_from_records($records);
}
/**
* Check whether or not the given user object exists.
*
* @param int $id the unique id the user.
* @return bool true if found, false otherwise.
*/
public function exists(int $id): bool {
global $DB;
return $DB->record_exists($this->ltiuserstable, ['id' => $id]);
}
/**
* Delete a user based on id.
*
* @param int $id the id of the user to remove.
*/
public function delete(int $id) {
global $DB;
$DB->delete_records($this->ltiuserstable, ['id' => $id]);
$DB->delete_records($this->userresourcelinkidtable, ['ltiuserid' => $id]);
}
/**
* Delete all lti user instances based on a given local deployment instance id.
*
* @param int $deploymentid the local id of the deployment instance to which the users belong.
*/
public function delete_by_deployment(int $deploymentid): void {
global $DB;
$DB->delete_records($this->ltiuserstable, ['ltideploymentid' => $deploymentid]);
}
}
@@ -0,0 +1,166 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\service;
use enrol_lti\local\ltiadvantage\entity\application_registration;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\context_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
/**
* Class application_registration_service.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class application_registration_service {
/** @var application_registration_repository repository to work with application_registration instances. */
private $appregistrationrepo;
/** @var deployment_repository repository to work with deployment instances. */
private $deploymentrepo;
/** @var resource_link_repository repository to work with resource link instances. */
private $resourcelinkrepo;
/** @var context_repository repository to work with context instances. */
private $contextrepo;
/** @var user_repository repository to work with user instances. */
private $userrepo;
/**
* The application_registration_service constructor.
*
* @param application_registration_repository $appregistrationrepo an application registration repository instance.
* @param deployment_repository $deploymentrepo a deployment repository instance.
* @param resource_link_repository $resourcelinkrepo a resource_link_repository instance.
* @param context_repository $contextrepo a context_repository instance.
* @param user_repository $userrepo a user_repository instance.
*/
public function __construct(application_registration_repository $appregistrationrepo,
deployment_repository $deploymentrepo, resource_link_repository $resourcelinkrepo,
context_repository $contextrepo, user_repository $userrepo) {
$this->appregistrationrepo = $appregistrationrepo;
$this->deploymentrepo = $deploymentrepo;
$this->resourcelinkrepo = $resourcelinkrepo;
$this->contextrepo = $contextrepo;
$this->userrepo = $userrepo;
}
/**
* Convert a DTO into a new application_registration domain object.
*
* @param \stdClass $dto the object containing information needed to register an application.
* @return application_registration the application_registration object
*/
private function registration_from_dto(\stdClass $dto): application_registration {
$registration = $this->appregistrationrepo->find($dto->id);
$registration->set_name($dto->name);
$registration->set_platformid(new \moodle_url($dto->platformid));
$registration->set_clientid($dto->clientid);
$registration->set_accesstokenurl(new \moodle_url($dto->accesstokenurl));
$registration->set_jwksurl(new \moodle_url($dto->jwksurl));
$registration->set_authenticationrequesturl(new \moodle_url($dto->authenticationrequesturl));
$registration->complete_registration();
return $registration;
}
/**
* Gets a unique id for the registration, with uniqueness guaranteed with a lookup.
*
* @return string the unique id.
*/
private function get_unique_id(): string {
global $DB;
do {
$bytes = random_bytes(30);
$uniqueid = bin2hex($bytes);
} while ($DB->record_exists('enrol_lti_app_registration', ['uniqueid' => $uniqueid]));
return $uniqueid;
}
/**
* Convert a DTO into a new DRAFT application_registration domain object.
*
* @param \stdClass $dto the object containing information needed to create the draft registration.
* @return application_registration the draft application_registration object
*/
private function draft_registration_from_dto(\stdClass $dto): application_registration {
return application_registration::create_draft(
$dto->name,
$this->get_unique_id()
);
}
/**
* Application service handling the use case "As an admin I can create a draft platform registration".
*
* @param \stdClass $appregdto details of the draft application to create.
* @return application_registration the application_registration domain object.
* @throws \coding_exception if the DTO doesn't contain required fields.
*/
public function create_draft_application_registration(\stdClass $appregdto): application_registration {
if (empty($appregdto->name)) {
throw new \coding_exception('Cannot create draft registration. Name is missing.');
}
$draftregistration = $this->draft_registration_from_dto($appregdto);
return $this->appregistrationrepo->save($draftregistration);
}
/**
* Application service handling the use case "As an admin I can update the registration of an LTI platform".
*
* @param \stdClass $appregdto details of the registration to update.
* @return application_registration the application_registration domain object.
*/
public function update_application_registration(\stdClass $appregdto): application_registration {
if (empty($appregdto->id)) {
throw new \coding_exception('Cannot update registration. Id is missing.');
}
return $this->appregistrationrepo->save($this->registration_from_dto($appregdto));
}
/**
* Application service handling the use case "As an admin I can delete a registration of an LTI platform".
*
* @param int $registrationid id of the registration to delete.
*/
public function delete_application_registration(int $registrationid): void {
$deployments = $this->deploymentrepo->find_all_by_registration($registrationid);
if ($deployments) {
$deploymentservice = new tool_deployment_service(
$this->appregistrationrepo,
$this->deploymentrepo,
$this->resourcelinkrepo,
$this->contextrepo,
$this->userrepo
);
foreach ($deployments as $deployment) {
$deploymentservice->delete_tool_deployment($deployment->get_id());
}
}
$this->appregistrationrepo->delete($registrationid);
}
}
@@ -0,0 +1,132 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\service;
use enrol_lti\local\ltiadvantage\entity\deployment;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\context_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
/**
* Class tool_deployment_service.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_deployment_service {
/** @var application_registration_repository repository to work with application_registration instances. */
private $appregistrationrepo;
/** @var deployment_repository repository to work with deployment instances. */
private $deploymentrepo;
/** @var resource_link_repository repository to work with resource link instances. */
private $resourcelinkrepo;
/** @var context_repository repository to work with context instances. */
private $contextrepo;
/** @var user_repository repository to work with user instances. */
private $userrepo;
/**
* The tool_deployment_service constructor.
*
* @param application_registration_repository $appregistrationrepo an application_registration_repository instance.
* @param deployment_repository $deploymentrepo a deployment_repository instance.
* @param resource_link_repository $resourcelinkrepo a resource_link_repository instance.
* @param context_repository $contextrepo a context_repository instance.
* @param user_repository $userrepo a user_repository instance.
*/
public function __construct(application_registration_repository $appregistrationrepo,
deployment_repository $deploymentrepo, resource_link_repository $resourcelinkrepo,
context_repository $contextrepo, user_repository $userrepo) {
$this->appregistrationrepo = $appregistrationrepo;
$this->deploymentrepo = $deploymentrepo;
$this->resourcelinkrepo = $resourcelinkrepo;
$this->contextrepo = $contextrepo;
$this->userrepo = $userrepo;
}
/**
* Service handling the use case "As an admin I can add a tool deployment to a platform registration".
*
* @param \stdClass $requestdto the required service data.
* @return deployment the deployment instance which has been created.
* @throws \coding_exception if the registration doesn't exist.
*/
public function add_tool_deployment(\stdClass $requestdto): deployment {
// DTO contains: registration_id, deployment_id, deployment_name.
[
'registration_id' => $registrationid,
'deployment_id' => $deploymentid,
'deployment_name' => $deploymentname
] = (array) $requestdto;
$registration = $this->appregistrationrepo->find($registrationid);
if (is_null($registration)) {
throw new \coding_exception("Cannot add deployment to non-existent application registration ".
"'$registrationid'");
}
$deployment = $registration->add_tool_deployment($deploymentname, $deploymentid);
return $this->deploymentrepo->save($deployment);
}
/**
* Service handling the use case "As an admin I can delete a tool deployment from a platform registration".
*
* @param int $deploymentid the id of the deployment to remove.
*/
public function delete_tool_deployment(int $deploymentid): void {
// Delete any resource links attached to this deployment.
$this->resourcelinkrepo->delete_by_deployment($deploymentid);
// Delete any context entries for the deployment.
$this->contextrepo->delete_by_deployment($deploymentid);
// Delete all enrolments for any users tied to this deployment.
global $DB, $CFG;
$sql = "SELECT lu.userid as ltiuserid, e.*
FROM {enrol_lti_users} lu
JOIN {enrol_lti_tools} lt
ON (lt.id = lu.toolid)
JOIN {enrol} e
ON (e.id = lt.enrolid)
WHERE lu.ltideploymentid = :deploymentid";
$instancesrs = $DB->get_recordset_sql($sql, ['deploymentid' => $deploymentid]);
require_once($CFG->dirroot . '/enrol/lti/lib.php');
$enrollti = new \enrol_lti_plugin();
foreach ($instancesrs as $instance) {
$userid = $instance->ltiuserid;
$enrollti->unenrol_user($instance, $userid);
}
$instancesrs->close();
// Delete any lti user enrolments.
$this->userrepo->delete_by_deployment($deploymentid);
// Delete the deployment itself.
$this->deploymentrepo->delete($deploymentid);
}
}
@@ -0,0 +1,386 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\service;
use enrol_lti\helper;
use enrol_lti\local\ltiadvantage\entity\context;
use enrol_lti\local\ltiadvantage\entity\deployment;
use enrol_lti\local\ltiadvantage\entity\migration_claim;
use enrol_lti\local\ltiadvantage\entity\resource_link;
use enrol_lti\local\ltiadvantage\entity\user;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\context_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
use Packback\Lti1p3\LtiMessageLaunch;
/**
* Class tool_launch_service.
*
* This class handles the launch of a resource by a user, using the LTI Advantage Resource Link Launch.
*
* See http://www.imsglobal.org/spec/lti/v1p3/#launch-from-a-resource-link
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_launch_service {
/** @var deployment_repository $deploymentrepo instance of a deployment repository. */
private $deploymentrepo;
/** @var application_registration_repository instance of a application_registration repository */
private $registrationrepo;
/** @var resource_link_repository instance of a resource_link repository */
private $resourcelinkrepo;
/** @var user_repository instance of a user repository*/
private $userrepo;
/** @var context_repository instance of a context repository */
private $contextrepo;
/**
* The tool_launch_service constructor.
*
* @param deployment_repository $deploymentrepo instance of a deployment_repository.
* @param application_registration_repository $registrationrepo instance of an application_registration_repository.
* @param resource_link_repository $resourcelinkrepo instance of a resource_link_repository.
* @param user_repository $userrepo instance of a user_repository.
* @param context_repository $contextrepo instance of a context_repository.
*/
public function __construct(deployment_repository $deploymentrepo,
application_registration_repository $registrationrepo, resource_link_repository $resourcelinkrepo,
user_repository $userrepo, context_repository $contextrepo) {
$this->deploymentrepo = $deploymentrepo;
$this->registrationrepo = $registrationrepo;
$this->resourcelinkrepo = $resourcelinkrepo;
$this->userrepo = $userrepo;
$this->contextrepo = $contextrepo;
}
/** Get the launch data from the launch.
*
* @param LtiMessageLaunch $launch the launch instance.
* @return \stdClass the launch data.
*/
private function get_launch_data(LtiMessageLaunch $launch): \stdClass {
$launchdata = $launch->getLaunchData();
$data = [
'platform' => $launchdata['iss'],
// The 'aud' property may be an array with one or more values, but can be a string if there is only one value.
// https://www.imsglobal.org/spec/security/v1p1#id-token.
'clientid' => is_array($launchdata['aud']) ? $launchdata['aud'][0] : $launchdata['aud'],
'exp' => $launchdata['exp'],
'nonce' => $launchdata['nonce'],
'sub' => $launchdata['sub'],
'roles' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/roles'],
'deploymentid' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/deployment_id'],
'context' => !empty($launchdata['https://purl.imsglobal.org/spec/lti/claim/context']) ?
$launchdata['https://purl.imsglobal.org/spec/lti/claim/context'] : null,
'resourcelink' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/resource_link'],
'targetlinkuri' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'],
'custom' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/custom'] ?? null,
'launchid' => $launch->getLaunchId(),
'user' => [
'givenname' => !empty($launchdata['given_name']) ? $launchdata['given_name'] : null,
'familyname' => !empty($launchdata['family_name']) ? $launchdata['family_name'] : null,
'name' => !empty($launchdata['name']) ? $launchdata['name'] : null,
'email' => !empty($launchdata['email']) ? $launchdata['email'] : null,
'picture' => !empty($launchdata['picture']) ? $launchdata['picture'] : null,
],
'ags' => $launchdata['https://purl.imsglobal.org/spec/lti-ags/claim/endpoint'] ?? null,
'nrps' => $launchdata['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice'] ?? null,
'lti1p1' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] ?? null
];
return (object) $data;
}
/**
* Get a context instance from the launch data.
*
* @param \stdClass $launchdata launch data.
* @param deployment $deployment the deployment to which the context belongs.
* @return context the context instance.
*/
private function context_from_launchdata(\stdClass $launchdata, deployment $deployment): context {
if ($context = $this->contextrepo->find_by_contextid($launchdata->context['id'], $deployment->get_id())) {
// The context has been mapped, just update it.
$context->set_types($launchdata->context['type']);
} else {
// Map a new context.
$context = $deployment->add_context($launchdata->context['id'], $launchdata->context['type']);
}
return $context;
}
/**
* Get a resource_link from the launch data.
*
* @param \stdClass $launchdata the launch data.
* @param \stdClass $resource the resource to which the resource link refers.
* @param deployment $deployment the deployment to which the resource_link belongs.
* @param context|null $context optional context in which the resource_link lives, null if not needed.
* @return resource_link the resource_link instance.
*/
private function resource_link_from_launchdata(\stdClass $launchdata, \stdClass $resource, deployment $deployment,
?context $context): resource_link {
if ($resourcelink = $this->resourcelinkrepo->find_by_deployment($deployment, $launchdata->resourcelink['id'])) {
// Resource link exists, so update it.
if (isset($context)) {
$resourcelink->set_contextid($context->get_id());
}
// A resource link may have been updated, via content item selection, to refer to a different resource.
if ($resourcelink->get_resourceid() != $resource->id) {
$resourcelink->set_resourceid($resource->id);
}
} else {
// Create a new resource link.
$resourcelink = $deployment->add_resource_link(
$launchdata->resourcelink['id'],
$resource->id,
$context ? $context->get_id() : null
);
}
// Add the AGS configuration for the resource link.
// See: http://www.imsglobal.org/spec/lti-ags/v2p0#assignment-and-grade-service-claim.
if ($launchdata->ags && (!empty($launchdata->ags['lineitems']) || !empty($launchdata->ags['lineitem']))) {
$resourcelink->add_grade_service(
!empty($launchdata->ags['lineitems']) ? new \moodle_url($launchdata->ags['lineitems']) : null,
!empty($launchdata->ags['lineitem']) ? new \moodle_url($launchdata->ags['lineitem']) : null,
$launchdata->ags['scope']
);
}
// NRPS.
if ($launchdata->nrps) {
$resourcelink->add_names_and_roles_service(
new \moodle_url($launchdata->nrps['context_memberships_url']),
$launchdata->nrps['service_versions']
);
}
return $resourcelink;
}
/**
* Get an lti user instance from the launch data.
*
* @param \stdClass $user the moodle user object.
* @param \stdClass $launchdata the launch data.
* @param \stdClass $resource the resource to which the user belongs.
* @param resource_link $resourcelink the resource_link from which the user originated.
* @return user the user instance.
*/
private function lti_user_from_launchdata(\stdClass $user, \stdClass $launchdata, \stdClass $resource,
resource_link $resourcelink): user {
// Find the user based on the unique-to-the-issuer 'sub' value.
if ($ltiuser = $this->userrepo->find_single_user_by_resource($user->id, $resource->id)) {
// User exists, so update existing based on resource data which may have changed.
$ltiuser->set_resourcelinkid($resourcelink->get_id());
$ltiuser->set_lang($resource->lang);
$ltiuser->set_city($resource->city);
$ltiuser->set_country($resource->country);
$ltiuser->set_institution($resource->institution);
$ltiuser->set_timezone($resource->timezone);
$ltiuser->set_maildisplay($resource->maildisplay);
} else {
// Create the lti user.
$ltiuser = $resourcelink->add_user(
$user->id,
$launchdata->sub,
$resource->lang,
$resource->city ?? '',
$resource->country ?? '',
$resource->institution ?? '',
$resource->timezone ?? '',
$resource->maildisplay ?? null,
);
}
$ltiuser->set_lastaccess(time());
return $ltiuser;
}
/**
* Get the migration claim from the launch data, or null if not found.
*
* @param \stdClass $launchdata the launch data.
* @return migration_claim|null the claim instance if present in the launch data, else null.
*/
private function migration_claim_from_launchdata(\stdClass $launchdata): ?migration_claim {
if (!isset($launchdata->lti1p1)) {
return null;
}
// Despite the spec requiring the oauth_consumer_key field be present in the migration claim:
// (see https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key),
// Platforms may omit this field making migration impossible.
// E.g. for Canvas launches taking place after an assignment_selection placement.
if (empty($launchdata->lti1p1['oauth_consumer_key'])) {
return null;
}
return new migration_claim($launchdata->lti1p1, $launchdata->deploymentid,
$launchdata->platform, $launchdata->clientid, $launchdata->exp, $launchdata->nonce,
new legacy_consumer_repository());
}
/**
* Check whether the launch user has an admin role.
*
* @param \stdClass $launchdata the launch data.
* @return bool true if the user is admin, false otherwise.
*/
private function user_is_admin(\stdClass $launchdata): bool {
// See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
if ($launchdata->roles) {
$adminroles = [
'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator',
'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator'
];
foreach ($adminroles as $validrole) {
if (in_array($validrole, $launchdata->roles)) {
return true;
}
}
}
return false;
}
/**
* Check whether the launch user is an instructor.
*
* @param \stdClass $launchdata the launch data.
* @param bool $includelegacy whether to also consider legacy simple names as valid roles.
* @return bool true if the user is an instructor, false otherwise.
*/
private function user_is_staff(\stdClass $launchdata, bool $includelegacy = false): bool {
// See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
// This method also provides support for (legacy, deprecated) simple names for context roles.
// I.e. 'ContentDeveloper' may be supported.
if ($launchdata->roles) {
$staffroles = [
'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper',
'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant'
];
if ($includelegacy) {
$staffroles[] = 'ContentDeveloper';
$staffroles[] = 'Instructor';
$staffroles[] = 'Instructor#TeachingAssistant';
}
foreach ($staffroles as $validrole) {
if (in_array($validrole, $launchdata->roles)) {
return true;
}
}
}
return false;
}
/**
* Handles the use case "A user launches the tool so they can view an external resource".
*
* @param \stdClass $user the Moodle user record, obtained via the auth_lti authentication process.
* @param LtiMessageLaunch $launch the launch data.
* @return array array containing [int $userid, \stdClass $resource]
* @throws \moodle_exception if launch problems are encountered.
*/
public function user_launches_tool(\stdClass $user, LtiMessageLaunch $launch): array {
$launchdata = $this->get_launch_data($launch);
if (!$registration = $this->registrationrepo->find_by_platform($launchdata->platform, $launchdata->clientid)) {
throw new \moodle_exception('ltiadvlauncherror:invalidregistration', 'enrol_lti', '',
[$launchdata->platform, $launchdata->clientid]);
}
if (!$deployment = $this->deploymentrepo->find_by_registration($registration->get_id(),
$launchdata->deploymentid)) {
throw new \moodle_exception('ltiadvlauncherror:invaliddeployment', 'enrol_lti', '',
[$launchdata->deploymentid]);
}
$resourceuuid = $launchdata->custom['id'] ?? null;
if (empty($resourceuuid)) {
throw new \moodle_exception('ltiadvlauncherror:missingid', 'enrol_lti');
}
$resource = array_values(helper::get_lti_tools(['uuid' => $resourceuuid]));
$resource = $resource[0] ?? null;
if (empty($resource) || $resource->status != ENROL_INSTANCE_ENABLED) {
throw new \moodle_exception('ltiadvlauncherror:invalidid', 'enrol_lti', '', $resourceuuid);
}
// Update the deployment with the legacy consumer_key information, allowing migration of users to take place in future
// names and roles syncs.
if ($migrationclaim = $this->migration_claim_from_launchdata($launchdata)) {
$deployment->set_legacy_consumer_key($migrationclaim->get_consumer_key());
$this->deploymentrepo->save($deployment);
}
// Save the context, if that claim is present.
$context = null;
if ($launchdata->context) {
$context = $this->context_from_launchdata($launchdata, $deployment);
$context = $this->contextrepo->save($context);
}
// Save the resource link for the tool deployment.
$resourcelink = $this->resource_link_from_launchdata($launchdata, $resource, $deployment, $context);
$resourcelink = $this->resourcelinkrepo->save($resourcelink);
// Save the user launching the resource link.
$ltiuser = $this->lti_user_from_launchdata($user, $launchdata, $resource, $resourcelink);
$ltiuser = $this->userrepo->save($ltiuser);
// Set the frame embedding mode, which controls the display of blocks and nav when launching.
global $SESSION;
$context = \context::instance_by_id($resource->contextid);
$isforceembed = $launchdata->custom['force_embed'] ?? false;
$isinstructor = $this->user_is_staff($launchdata, true) || $this->user_is_admin($launchdata);
$isforceembed = $isforceembed || ($context->contextlevel == CONTEXT_MODULE && !$isinstructor);
if ($isforceembed) {
$SESSION->forcepagelayout = 'embedded';
} else {
unset($SESSION->forcepagelayout);
}
// Enrol the user in the course with no role.
$result = helper::enrol_user($resource, $ltiuser->get_localid());
if ($result !== helper::ENROLMENT_SUCCESSFUL) {
throw new \moodle_exception($result, 'enrol_lti');
}
// Give the user the role in the given context.
$roleid = $isinstructor ? $resource->roleinstructor : $resource->rolelearner;
role_assign($roleid, $ltiuser->get_localid(), $resource->contextid);
return [$ltiuser->get_localid(), $resource];
}
}
@@ -0,0 +1,254 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\table;
use enrol_lti\helper;
defined('MOODLE_INTERNAL') || die;
global $CFG;
require_once($CFG->libdir . '/tablelib.php');
/**
* Class which displays a list of resources published over LTI Advantage.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class published_resources_table extends \table_sql {
/**
* @var \enrol_plugin $ltiplugin
*/
protected $ltiplugin;
/**
* @var bool $ltienabled
*/
protected $ltienabled;
/**
* @var bool $canconfig
*/
protected $canconfig;
/**
* @var int $courseid The course id.
*/
protected $courseid;
/**
* Sets up the table.
*
* @param string $courseid The id of the course.
*/
public function __construct($courseid) {
parent::__construct('enrol_lti_manage_table');
$this->define_columns(array(
'name',
'launch',
'edit'
));
$this->define_headers(array(
get_string('name'),
get_string('lti13launchdetails', 'enrol_lti'),
get_string('edit')
));
$this->collapsible(false);
$this->sortable(false);
// Set the variables we need access to.
$this->ltiplugin = enrol_get_plugin('lti');
$this->ltienabled = enrol_is_enabled('lti');
$this->canconfig = has_capability('moodle/course:enrolconfig', \context_course::instance($courseid));
$this->courseid = $courseid;
// Set help icons.
$launchicon = new \help_icon('lti13launchdetails', 'enrol_lti');
$this->define_help_for_headers(['1' => $launchicon]);
}
/**
* Generate the name column.
*
* @param \stdClass $tool event data.
* @return string
*/
public function col_name($tool) {
$toolcontext = \context::instance_by_id($tool->contextid, IGNORE_MISSING);
$name = $toolcontext ? helper::get_name($tool) : $this->get_deleted_activity_name_html($tool);
return $this->get_display_text($tool, $name);
}
/**
* Generate the launch column.
*
* @param \stdClass $tool instance data.
* @return string
*/
public function col_launch($tool) {
global $OUTPUT;
$customparamslabel = get_string('customproperties', 'enrol_lti');
$customparams = "id={$tool->uuid}";
$launchurl = new \moodle_url('/enrol/lti/launch.php');
$launchurllabel = get_string('launchurl', 'enrol_lti');
$data = [
"rows" => [
[
"label" => $launchurllabel,
"text" => $launchurl->out(false),
"id" => "launchurl",
"hidelabel" => false
],
[
"label" => $customparamslabel,
"text" => $customparams,
"id" => "customparams",
"hidelabel" => false
]
]
];
$return = $OUTPUT->render_from_template("enrol_lti/copy_grid", $data);
return $return;
}
/**
* Generate the edit column.
*
* @param \stdClass $tool event data.
* @return string
*/
public function col_edit($tool) {
global $OUTPUT;
$buttons = array();
$instance = new \stdClass();
$instance->id = $tool->enrolid;
$instance->courseid = $tool->courseid;
$instance->enrol = 'lti';
$instance->status = $tool->status;
$strdelete = get_string('delete');
$strenable = get_string('enable');
$strdisable = get_string('disable');
$url = new \moodle_url('/enrol/lti/index.php', array('sesskey' => sesskey(), 'courseid' => $this->courseid));
if ($this->ltiplugin->can_delete_instance($instance)) {
$aurl = new \moodle_url($url, array('action' => 'delete', 'instanceid' => $instance->id));
$buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/delete', $strdelete, 'core',
array('class' => 'iconsmall')));
}
if ($this->ltienabled && $this->ltiplugin->can_hide_show_instance($instance)) {
if ($instance->status == ENROL_INSTANCE_ENABLED) {
$aurl = new \moodle_url($url, array('action' => 'disable', 'instanceid' => $instance->id));
$buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/hide', $strdisable, 'core',
array('class' => 'iconsmall')));
} else if ($instance->status == ENROL_INSTANCE_DISABLED) {
$aurl = new \moodle_url($url, array('action' => 'enable', 'instanceid' => $instance->id));
$buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/show', $strenable, 'core',
array('class' => 'iconsmall')));
}
}
if ($this->ltienabled && $this->canconfig) {
$linkparams = array(
'courseid' => $instance->courseid,
'id' => $instance->id, 'type' => $instance->enrol,
'returnurl' => new \moodle_url('/enrol/lti/index.php', array('courseid' => $this->courseid))
);
$editlink = new \moodle_url("/enrol/editinstance.php", $linkparams);
$buttons[] = $OUTPUT->action_icon($editlink, new \pix_icon('t/edit', get_string('edit'), 'core',
array('class' => 'iconsmall')));
}
return implode(' ', $buttons);
}
/**
* Query the reader. Store results in the object for use by build_table.
*
* @param int $pagesize size of page for paginated displayed table.
* @param bool $useinitialsbar do you want to use the initials bar.
*/
public function query_db($pagesize, $useinitialsbar = true) {
$total = helper::count_lti_tools(['courseid' => $this->courseid, 'ltiversion' => 'LTI-1p3']);
$this->pagesize($pagesize, $total);
$tools = helper::get_lti_tools(['courseid' => $this->courseid, 'ltiversion' => 'LTI-1p3'],
$this->get_page_start(), $this->get_page_size());
$this->rawdata = $tools;
// Set initial bars.
if ($useinitialsbar) {
$this->initialbars($total > $pagesize);
}
}
/**
* Returns text to display in the columns.
*
* @param \stdClass $tool the tool
* @param string $text the text to alter
* @return string
*/
protected function get_display_text($tool, $text) {
if ($tool->status != ENROL_INSTANCE_ENABLED) {
return \html_writer::tag('div', $text, array('class' => 'dimmed_text'));
}
return $text;
}
/**
* Get a warning icon, with tooltip, describing enrolment instances sharing activities which have been deleted.
*
* @param \stdClass $tool the tool instance record.
* @return string the HTML for the name column.
*/
protected function get_deleted_activity_name_html(\stdClass $tool): string {
global $OUTPUT;
$icon = \html_writer::tag(
'a',
$OUTPUT->pix_icon('enrolinstancewarning', get_string('deletedactivityalt' , 'enrol_lti'), 'enrol_lti'), [
"class" => "btn btn-link p-0",
"role" => "button",
"data-container" => "body",
"data-toggle" => "popover",
"data-placement" => right_to_left() ? "left" : "right",
"data-content" => get_string('deletedactivitydescription', 'enrol_lti'),
"data-html" => "true",
"tabindex" => "0",
"data-trigger" => "focus"
]
);
$name = \html_writer::span($icon . get_string('deletedactivity', 'enrol_lti'));
if ($tool->name) {
$name .= \html_writer::empty_tag('br') . \html_writer::empty_tag('br') . $tool->name;
}
return $name;
}
}
@@ -0,0 +1,75 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\task;
use core\task\scheduled_task;
use enrol_lti\helper;
/**
* LTI Advantage task responsible for pushing grades to tool platforms.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sync_grades extends scheduled_task {
/**
* Get a descriptive name for this task.
*
* @return string
*/
public function get_name() {
return get_string('tasksyncgrades', 'enrol_lti');
}
/**
* Creates adhoc tasks (one per resource) to synchronize grades from the tool to any registered platforms.
*
* @return bool|void
*/
public function execute() {
if (!is_enabled_auth('lti')) {
mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')));
return true;
}
if (!enrol_is_enabled('lti')) {
mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'));
return true;
}
$resources = helper::get_lti_tools([
'status' => ENROL_INSTANCE_ENABLED,
'gradesync' => 1,
'ltiversion' => 'LTI-1p3'
]);
if (empty($resources)) {
mtrace('Skipping task - There are no resources with grade sync enabled.');
return true;
}
foreach ($resources as $resource) {
$task = new \enrol_lti\local\ltiadvantage\task\sync_tool_grades();
$task->set_custom_data($resource);
$task->set_component('enrol_lti');
\core\task\manager::queue_adhoc_task($task, true);
}
mtrace('Spawned ' . count($resources) . ' adhoc tasks to sync grades.');
}
}
@@ -0,0 +1,454 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\task;
use core\http_client;
use core\task\scheduled_task;
use enrol_lti\helper;
use enrol_lti\local\ltiadvantage\entity\application_registration;
use enrol_lti\local\ltiadvantage\entity\nrps_info;
use enrol_lti\local\ltiadvantage\entity\resource_link;
use enrol_lti\local\ltiadvantage\entity\user;
use enrol_lti\local\ltiadvantage\lib\issuer_database;
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
use Packback\Lti1p3\LtiNamesRolesProvisioningService;
use Packback\Lti1p3\LtiRegistration;
use Packback\Lti1p3\LtiServiceConnector;
use stdClass;
/**
* LTI Advantage-specific task responsible for syncing memberships from tool platforms with the tool.
*
* This task may gather members from a context-level service call, depending on whether a resource-level service call
* (which is made first) was successful. Because of the context-wide memberships, and because each published resource
* has per-resource access control (role assignments), this task only enrols user into the course, and does not assign
* roles to resource/course contexts. Role assignment only takes place during a launch, via the tool_launch_service.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sync_members extends scheduled_task {
/** @var array Array of user photos. */
protected $userphotos = [];
/** @var resource_link_repository $resourcelinkrepo for fetching resource_link instances.*/
protected $resourcelinkrepo;
/** @var application_registration_repository $appregistrationrepo for fetching application_registration instances.*/
protected $appregistrationrepo;
/** @var deployment_repository $deploymentrepo for fetching deployment instances. */
protected $deploymentrepo;
/** @var user_repository $userrepo for fetching and saving lti user information.*/
protected $userrepo;
/** @var issuer_database $issuerdb library specific registration DB required to create service connectors.*/
protected $issuerdb;
/**
* Get the name for this task.
*
* @return string the name of the task.
*/
public function get_name(): string {
return get_string('tasksyncmembers', 'enrol_lti');
}
/**
* Make a resource-link-level memberships call.
*
* @param nrps_info $nrps information about names and roles service endpoints and scopes.
* @param LtiServiceConnector $sc a service connector object.
* @param LtiRegistration $registration the registration
* @param resource_link $resourcelink the resource link
* @return array an array of members if found.
*/
protected function get_resource_link_level_members(nrps_info $nrps, LtiServiceConnector $sc, LtiRegistration $registration,
resource_link $resourcelink) {
// Try a resource-link-level memberships call first, falling back to context-level if no members are found.
$reslinkmembershipsurl = $nrps->get_context_memberships_url();
$reslinkmembershipsurl->param('rlid', $resourcelink->get_resourcelinkid());
$servicedata = [
'context_memberships_url' => $reslinkmembershipsurl->out(false)
];
$reslinklevelnrps = new LtiNamesRolesProvisioningService($sc, $registration, $servicedata);
mtrace('Making resource-link-level memberships request');
return $reslinklevelnrps->getMembers();
}
/**
* Make a context-level memberships call.
*
* @param nrps_info $nrps information about names and roles service endpoints and scopes.
* @param LtiServiceConnector $sc a service connector object.
* @param LtiRegistration $registration the registration
* @return array an array of members.
*/
protected function get_context_level_members(nrps_info $nrps, LtiServiceConnector $sc, LtiRegistration $registration) {
$clservicedata = [
'context_memberships_url' => $nrps->get_context_memberships_url()->out(false)
];
$contextlevelnrps = new LtiNamesRolesProvisioningService($sc, $registration, $clservicedata);
return $contextlevelnrps->getMembers();
}
/**
* Make the NRPS service call and fetch members based on the given resource link.
*
* Memberships will be retrieved by first trying the link-level memberships service first, falling back to calling
* the context-level memberships service only if the link-level call fails.
*
* @param application_registration $appregistration an application registration instance.
* @param resource_link $resourcelink a resourcelink instance.
* @return array an array of members.
*/
protected function get_members_from_resource_link(application_registration $appregistration,
resource_link $resourcelink) {
// Get a service worker for the corresponding application registration.
$registration = $this->issuerdb->findRegistrationByIssuer(
$appregistration->get_platformid()->out(false),
$appregistration->get_clientid()
);
global $CFG;
require_once($CFG->libdir . '/filelib.php');
$sc = new LtiServiceConnector(new launch_cache_session(), new http_client());
$nrps = $resourcelink->get_names_and_roles_service();
try {
$members = $this->get_resource_link_level_members($nrps, $sc, $registration, $resourcelink);
} catch (\Exception $e) {
mtrace('Link-level memberships request failed. Making context-level memberships request');
$members = $this->get_context_level_members($nrps, $sc, $registration);
}
return $members;
}
/**
* Performs the synchronisation of members.
*/
public function execute() {
if (!is_enabled_auth('lti')) {
mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')));
return;
}
if (!enrol_is_enabled('lti')) {
mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'));
return;
}
$this->resourcelinkrepo = new resource_link_repository();
$this->appregistrationrepo = new application_registration_repository();
$this->deploymentrepo = new deployment_repository();
$this->userrepo = new user_repository();
$this->issuerdb = new issuer_database($this->appregistrationrepo, $this->deploymentrepo);
$resources = helper::get_lti_tools(['status' => ENROL_INSTANCE_ENABLED, 'membersync' => 1,
'ltiversion' => 'LTI-1p3']);
foreach ($resources as $resource) {
mtrace("Starting - Member sync for published resource '$resource->id' for course '$resource->courseid'.");
$usercount = 0;
$enrolcount = 0;
$unenrolcount = 0;
$syncedusers = [];
// Get all resource_links for this shared resource.
// This is how context/resource_link memberships calls will be made.
$resourcelinks = $this->resourcelinkrepo->find_by_resource((int)$resource->id);
foreach ($resourcelinks as $resourcelink) {
mtrace("Requesting names and roles for the resource link '{$resourcelink->get_id()}' for the resource" .
" '{$resource->id}'");
if (!$resourcelink->get_names_and_roles_service()) {
mtrace("Skipping - No names and roles service found.");
continue;
}
$appregistration = $this->appregistrationrepo->find_by_deployment(
$resourcelink->get_deploymentid()
);
if (!$appregistration) {
mtrace("Skipping - no corresponding application registration found.");
continue;
}
try {
$members = $this->get_members_from_resource_link($appregistration, $resourcelink);
} catch (\Exception $e) {
mtrace("Skipping - Names and Roles service request failed: {$e->getMessage()}.");
continue;
}
// Fetched members count.
$membercount = count($members);
$usercount += $membercount;
mtrace("$membercount members received.");
// Process member information.
[$rlenrolcount, $userids] = $this->sync_member_information($appregistration, $resource,
$resourcelink, $members);
$enrolcount += $rlenrolcount;
// Update the list of users synced for this shared resource or its context.
$syncedusers = array_unique(array_merge($syncedusers, $userids));
mtrace("Completed - Synced $membercount members for the resource link '{$resourcelink->get_id()}' ".
"for the resource '{$resource->id}'.\n");
// Sync unenrolments on a per-resource-link basis so we have fine grained control over unenrolments.
// If a resource link doesn't support NRPS, it will already have been skipped.
$unenrolcount += $this->sync_unenrol_resourcelink($resourcelink, $resource, $syncedusers);
}
mtrace("Completed - Synced members for tool '$resource->id' in the course '$resource->courseid'. " .
"Processed $usercount users; enrolled $enrolcount members; unenrolled $unenrolcount members.\n");
}
if (!empty($resources) && !empty($this->userphotos)) {
// Sync the user profile photos.
mtrace("Started - Syncing user profile images.");
$countsyncedimages = $this->sync_profile_images();
mtrace("Completed - Synced $countsyncedimages profile images.");
}
}
/**
* Process unenrolment of users for a given resource link and based on the list of recently synced users.
*
* @param resource_link $resourcelink the resource_link instance to which the $synced users pertains
* @param stdClass $resource the resource object instance
* @param array $syncedusers the array of recently synced users, who are not to be unenrolled.
* @return int the number of unenrolled users.
*/
protected function sync_unenrol_resourcelink(resource_link $resourcelink, stdClass $resource,
array $syncedusers): int {
if (!$this->should_sync_unenrol($resource->membersyncmode)) {
return 0;
}
$ltiplugin = enrol_get_plugin('lti');
$unenrolcount = 0;
// Get all users for the resource_link instance.
$linkusers = $this->userrepo->find_by_resource_link($resourcelink->get_id());
foreach ($linkusers as $ltiuser) {
if (!in_array($ltiuser->get_localid(), $syncedusers)) {
$instance = new stdClass();
$instance->id = $resource->enrolid;
$instance->courseid = $resource->courseid;
$instance->enrol = 'lti';
$ltiplugin->unenrol_user($instance, $ltiuser->get_localid());
$unenrolcount++;
}
}
return $unenrolcount;
}
/**
* Check whether the member has an instructor role or not.
*
* @param array $member
* @return bool
*/
protected function member_is_instructor(array $member): bool {
// See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
$memberroles = $member['roles'];
if ($memberroles) {
$adminroles = [
'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator',
'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator'
];
$staffroles = [
'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper',
'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant',
'ContentDeveloper',
'Instructor',
'Instructor#TeachingAssistant'
];
$instructorroles = array_merge($adminroles, $staffroles);
foreach ($instructorroles as $validrole) {
if (in_array($validrole, $memberroles)) {
return true;
}
}
}
return false;
}
/**
* Method to determine whether to sync unenrolments or not.
*
* @param int $syncmode The shared resource's membersyncmode.
* @return bool true if unenrolment should be synced, false if not.
*/
protected function should_sync_unenrol($syncmode): bool {
return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_UNENROL_MISSING;
}
/**
* Method to determine whether to sync enrolments or not.
*
* @param int $syncmode The shared resource's membersyncmode.
* @return bool true if enrolment should be synced, false if not.
*/
protected function should_sync_enrol($syncmode): bool {
return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_ENROL_NEW;
}
/**
* Creates an lti user object from a member entry.
*
* @param stdClass $user the Moodle user record representing this member.
* @param stdClass $resource the locally published resource record, used for setting user defaults.
* @param resource_link $resourcelink the resource_link instance.
* @param array $member the member information from the NRPS service call.
* @return user the lti user instance.
*/
protected function ltiuser_from_member(stdClass $user, stdClass $resource,
resource_link $resourcelink, array $member): user {
if (!$ltiuser = $this->userrepo->find_single_user_by_resource($user->id, $resource->id)) {
// New user, so create them.
$ltiuser = user::create(
$resourcelink->get_resourceid(),
$user->id,
$resourcelink->get_deploymentid(),
$member['user_id'],
$resource->lang,
$resource->timezone,
$resource->city ?? '',
$resource->country ?? '',
$resource->institution ?? '',
$resource->maildisplay
);
}
$ltiuser->set_lastaccess(time());
return $ltiuser;
}
/**
* Performs synchronisation of member information and enrolments.
*
* @param application_registration $appregistration the application_registration instance.
* @param stdClass $resource the enrol_lti_tools resource information.
* @param resource_link $resourcelink the resource_link instance.
* @param user[] $members an array of members to sync.
* @return array An array containing the counts of enrolled users and a list of userids.
*/
protected function sync_member_information(application_registration $appregistration, stdClass $resource,
resource_link $resourcelink, array $members): array {
$enrolcount = 0;
$userids = [];
// Get the verified legacy consumer key, if mapped, from the resource link's tool deployment.
// This will be used to locate legacy user accounts and link them to LTI 1.3 users.
// A launch must have been made in order to get the legacy consumer key from the lti1p1 migration claim.
$deployment = $this->deploymentrepo->find($resourcelink->get_deploymentid());
$legacyconsumerkey = $deployment->get_legacy_consumer_key() ?? '';
foreach ($members as $member) {
$auth = get_auth_plugin('lti');
if ($auth->get_user_binding($appregistration->get_platformid()->out(false), $member['user_id'])) {
// Use is bound already, so we can update them.
$user = $auth->find_or_create_user_from_membership($member, $appregistration->get_platformid()->out(false));
if ($user->auth != 'lti') {
mtrace("Skipped profile sync for user '$user->id'. The user does not belong to the LTI auth method.");
}
} else {
// Not bound, so defer to the role-based provisioning mode for the resource.
$provisioningmode = $this->member_is_instructor($member) ? $resource->provisioningmodeinstructor :
$resource->provisioningmodelearner;
switch ($provisioningmode) {
case \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY:
// Automatic provisioning - this will create a user account and log the user in.
$user = $auth->find_or_create_user_from_membership($member, $appregistration->get_platformid()->out(false),
$legacyconsumerkey);
break;
case \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING:
case \auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY:
default:
mtrace("Skipping account creation for member '{$member['user_id']}'. This member is not eligible for ".
"automatic creation due to the current account provisioning mode.");
continue 2;
}
}
$ltiuser = $this->ltiuser_from_member($user, $resource, $resourcelink, $member);
if ($this->should_sync_enrol($resource->membersyncmode)) {
$ltiuser->set_resourcelinkid($resourcelink->get_id());
$ltiuser = $this->userrepo->save($ltiuser);
if ($user->auth != 'lti') {
mtrace("Skipped picture sync for user '$user->id'. The user does not belong to the LTI auth method.");
} else {
if (isset($member['picture'])) {
$this->userphotos[$ltiuser->get_localid()] = $member['picture'];
}
}
// Enrol the user in the course.
if (helper::enrol_user($resource, $ltiuser->get_localid()) === helper::ENROLMENT_SUCCESSFUL) {
$enrolcount++;
}
}
// If the member has been created, or exists locally already, mark them as valid so as to not unenrol them
// when syncing memberships for shared resources configured as either MEMBER_SYNC_ENROL_AND_UNENROL or
// MEMBER_SYNC_UNENROL_MISSING.
$userids[] = $user->id;
}
return [$enrolcount, $userids];
}
/**
* Performs synchronisation of user profile images.
*
* @return int the count of synced photos.
*/
protected function sync_profile_images(): int {
$counter = 0;
foreach ($this->userphotos as $userid => $url) {
if ($url) {
$result = helper::update_user_profile_image($userid, $url);
if ($result === helper::PROFILE_IMAGE_UPDATE_SUCCESSFUL) {
$counter++;
mtrace("Profile image successfully downloaded and created for user '$userid' from $url.");
} else {
mtrace($result);
}
}
}
return $counter;
}
}
@@ -0,0 +1,274 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\task;
use core\http_client;
use core\task\adhoc_task;
use enrol_lti\local\ltiadvantage\lib\issuer_database;
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
use Packback\Lti1p3\LtiAssignmentsGradesService;
use Packback\Lti1p3\LtiGrade;
use Packback\Lti1p3\LtiLineitem;
use Packback\Lti1p3\LtiRegistration;
use Packback\Lti1p3\LtiServiceConnector;
/**
* LTI Advantage task responsible for pushing grades to tool platforms.
*
* @package enrol_lti
* @copyright 2023 David Pesce <david.pesce@exputo.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sync_tool_grades extends adhoc_task {
/**
* Sync grades to the platform using the Assignment and Grade Services (AGS).
*
* @param \stdClass $resource the enrol_lti_tools data record for the shared resource.
* @return array an array containing the
*/
protected function sync_grades_for_resource($resource): array {
$usercount = 0;
$sendcount = 0;
$userrepo = new user_repository();
$resourcelinkrepo = new resource_link_repository();
$appregistrationrepo = new application_registration_repository();
$issuerdb = new issuer_database($appregistrationrepo, new deployment_repository());
if ($users = $userrepo->find_by_resource($resource->id)) {
$completion = new \completion_info(get_course($resource->courseid));
$syncedusergrades = []; // Keep track of those users who have had their grade synced during this run.
foreach ($users as $user) {
$mtracecontent = "for the user '{$user->get_localid()}', for the resource '$resource->id' and the course " .
"'$resource->courseid'";
$usercount++;
// Check if we do not have a grade service endpoint in either of the resource links.
// Remember, not all launches need to support grade services.
$userresourcelinks = $resourcelinkrepo->find_by_resource_and_user($resource->id, $user->get_id());
$userlastgrade = $user->get_lastgrade();
mtrace("Found ".count($userresourcelinks)." resource link(s) $mtracecontent. Attempting to sync grades for all.");
foreach ($userresourcelinks as $userresourcelink) {
mtrace("Processing resource link '{$userresourcelink->get_resourcelinkid()}'.");
if (!$gradeservice = $userresourcelink->get_grade_service()) {
mtrace("Skipping - No grade service found $mtracecontent.");
continue;
}
if (!$context = \context::instance_by_id($resource->contextid, IGNORE_MISSING)) {
mtrace("Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.");
continue;
}
$grade = false;
$dategraded = false;
if ($context->contextlevel == CONTEXT_COURSE) {
if ($resource->gradesynccompletion && !$completion->is_course_complete($user->get_localid())) {
mtrace("Skipping - Course not completed $mtracecontent.");
continue;
}
// Get the grade.
if ($grade = grade_get_course_grade($user->get_localid(), $resource->courseid)) {
$grademax = floatval($grade->item->grademax);
$dategraded = $grade->dategraded;
$grade = $grade->grade;
}
} else if ($context->contextlevel == CONTEXT_MODULE) {
$cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
if ($resource->gradesynccompletion) {
$data = $completion->get_data($cm, false, $user->get_localid());
if (!in_array($data->completionstate, [COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE])) {
mtrace("Skipping - Activity not completed $mtracecontent.");
continue;
}
}
$grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance,
$user->get_localid());
if (!empty($grades->items[0]->grades)) {
$grade = reset($grades->items[0]->grades);
if (!empty($grade->item)) {
$grademax = floatval($grade->item->grademax);
} else {
$grademax = floatval($grades->items[0]->grademax);
}
$dategraded = $grade->dategraded;
$grade = $grade->grade;
}
}
if ($grade === false || $grade === null || strlen($grade) < 1) {
mtrace("Skipping - Invalid grade $mtracecontent.");
continue;
}
if (empty($grademax)) {
mtrace("Skipping - Invalid grademax $mtracecontent.");
continue;
}
if (!grade_floats_different($grade, $userlastgrade)) {
mtrace("Not sent - The grade $mtracecontent was not sent as the grades are the same.");
continue;
}
$floatgrade = $grade / $grademax;
try {
// Get an AGS instance for the corresponding application registration and service data.
$appregistration = $appregistrationrepo->find_by_deployment(
$userresourcelink->get_deploymentid()
);
$registration = $issuerdb->findRegistrationByIssuer(
$appregistration->get_platformid()->out(false),
$appregistration->get_clientid()
);
global $CFG;
require_once($CFG->libdir . '/filelib.php');
$sc = new LtiServiceConnector(new launch_cache_session(), new http_client());
$lineitemurl = $gradeservice->get_lineitemurl();
$lineitemsurl = $gradeservice->get_lineitemsurl();
$servicedata = [
'lineitems' => $lineitemsurl ? $lineitemsurl->out(false) : null,
'lineitem' => $lineitemurl ? $lineitemurl->out(false) : null,
'scope' => $gradeservice->get_scopes(),
];
$ags = $this->get_ags($sc, $registration, $servicedata);
$ltigrade = LtiGrade::new()
->setScoreGiven($grade)
->setScoreMaximum($grademax)
->setUserId($user->get_sourceid())
->setTimestamp(date(\DateTimeInterface::ISO8601, $dategraded))
->setActivityProgress('Completed')
->setGradingProgress('FullyGraded');
if (empty($servicedata['lineitem'])) {
// The launch did not include a couple lineitem, so find or create the line item for grading.
$lineitem = $ags->findOrCreateLineitem(new LtiLineitem([
'label' => $this->get_line_item_label($resource, $context),
'scoreMaximum' => $grademax,
'tag' => 'grade',
'resourceId' => $userresourcelink->get_resourceid(),
'resourceLinkId' => $userresourcelink->get_resourcelinkid()
]));
$response = $ags->putGrade($ltigrade, $lineitem);
} else {
// Let AGS find the coupled line item.
$response = $ags->putGrade($ltigrade);
}
} catch (\Exception $e) {
mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
mtrace($e->getMessage());
continue;
}
$successresponses = [200, 201, 202, 204];
if (in_array($response['status'], $successresponses)) {
$user->set_lastgrade(grade_floatval($grade));
$syncedusergrades[$user->get_id()] = $user;
mtrace("Success - The grade '$floatgrade' $mtracecontent was sent.");
} else {
mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
mtrace("Header: {$response['headers']['httpstatus']}");
}
}
}
// Update the lastgrade value for any users who had a grade synced. Allows skipping on future runs if not changed.
// Update the count of total users having their grades synced, not the total number of grade sync calls made.
foreach ($syncedusergrades as $ltiuser) {
$userrepo->save($ltiuser);
$sendcount = $sendcount + 1;
}
}
return [$usercount, $sendcount];
}
/**
* Get the string label for the line item associated with the resource, based on the course or module name.
*
* @param \stdClass $resource the enrol_lti_tools record.
* @param \context $context the context of the resource - either course or module.
* @return string the label to use in the line item.
*/
protected function get_line_item_label(\stdClass $resource, \context $context): string {
$resourcename = 'default';
if ($context->contextlevel == CONTEXT_COURSE) {
global $DB;
$coursenamesql = "SELECT c.fullname
FROM {enrol_lti_tools} t
JOIN {enrol} e
ON (e.id = t.enrolid)
JOIN {course} c
ON (c.id = e.courseid)
WHERE t.id = :resourceid";
$coursename = $DB->get_field_sql($coursenamesql, ['resourceid' => $resource->id]);
$resourcename = format_string($coursename, true, ['context' => $context->id]);
} else if ($context->contextlevel == CONTEXT_MODULE) {
foreach (get_fast_modinfo($resource->courseid)->get_cms() as $mod) {
if ($mod->context->id == $context->id) {
$resourcename = $mod->name;
}
}
}
return $resourcename;
}
/**
* Get an Assignment and Grade Services (AGS) instance to make the call to the platform.
*
* @param LtiServiceConnector $sc a service connector instance.
* @param LtiRegistration $registration the registration instance.
* @param array $sd the service data.
* @return LtiAssignmentsGradesService
*/
protected function get_ags(LtiServiceConnector $sc, LtiRegistration $registration, array $sd): LtiAssignmentsGradesService {
return new LtiAssignmentsGradesService($sc, $registration, $sd);
}
/**
* Performs the synchronisation of grades from the tool to any registered platforms.
*
* @return bool|void
*/
public function execute() {
global $CFG;
require_once($CFG->dirroot . '/lib/completionlib.php');
require_once($CFG->libdir . '/gradelib.php');
require_once($CFG->dirroot . '/grade/querylib.php');
$resource = $this->get_custom_data();
mtrace("Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$resource->courseid'.");
[$usercount, $sendcount] = $this->sync_grades_for_resource($resource);
mtrace("Completed - Synced grades for tool '$resource->id' in the course '$resource->courseid'. " .
"Processed $usercount users; sent $sendcount grades.");
mtrace("");
}
}
@@ -0,0 +1,95 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\utility;
/**
* Utility class for LTI Advantage messages.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class message_helper {
/**
* Determine if the LTI roles in the launch contains any instructor or admin roles.
*
* @param array $jwtdata array formatted JWT data from the launch.
* @return bool true if the roles contain a constructor role, false otherwise.
*/
public static function is_instructor_launch(array $jwtdata): bool {
return self::user_is_admin($jwtdata) || self::user_is_staff($jwtdata, true);
}
/**
* Check whether the launch user is an instructor.
*
* @param array $jwtdata array formatted JWT data from the launch.
* @param bool $includelegacyroles whether to also consider legacy simple names as valid roles.
* @return bool true if the user is an instructor, false otherwise.
*/
private static function user_is_staff(array $jwtdata, bool $includelegacyroles = false): bool {
// See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
// This method also provides support for (legacy, deprecated) simple names for context roles.
// I.e. 'ContentDeveloper' may be supported.
$launchroles = $jwtdata['https://purl.imsglobal.org/spec/lti/claim/roles'] ?? null;
if ($launchroles) {
$staffroles = [
'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper',
'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant'
];
if ($includelegacyroles) {
$staffroles[] = 'ContentDeveloper';
$staffroles[] = 'Instructor';
$staffroles[] = 'Instructor#TeachingAssistant';
}
foreach ($staffroles as $validrole) {
if (in_array($validrole, $launchroles)) {
return true;
}
}
}
return false;
}
/**
* Check whether the launch user has an admin role.
*
* @param array $jwtdata array formatted JWT data from the launch.
* @return bool true if the user is admin, false otherwise.
*/
private static function user_is_admin(array $jwtdata): bool {
// See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
$launchroles = $jwtdata['https://purl.imsglobal.org/spec/lti/claim/roles'] ?? null;
if ($launchroles) {
$adminroles = [
'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator',
'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator'
];
foreach ($adminroles as $validrole) {
if (in_array($validrole, $launchroles)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,164 @@
<?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/>.
namespace enrol_lti\local\ltiadvantage\viewobject;
/**
* The class published_resource, instances of which represent a specific VIEW of a published resource.
*
* This class performs no validation and is only meant to be used as a slice of the existing data for use in the
* content selection flow.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class published_resource {
/** @var string the name of this resource. */
private $name;
/** @var string full name of the course to which this published resource belongs. */
private $coursefullname;
/** @var int id of the course to which this published resource belongs. */
private $courseid;
/** @var int the context id of the resource */
private $contextid;
/** @var int id of the enrol_lti_tools instance (i.e. the id of the 'published resource'). */
private $id;
/** @var string a v4 uuid identifier for this published resource. */
private $uuid;
/** @var bool whether or not this resource supports grades. */
private $supportsgrades;
/** @var float the max grade or null if not a graded resource. */
private $grademax;
/** @var bool whether or not this resource is itself a course. */
private $iscourse;
/**
* The published_resource constructor.
*
* @param string $name the name of this resource.
* @param string $coursefullname full name of the course to which this published resource belongs.
* @param int $courseid id of the course to which this published resource belongs.
* @param int $contextid id of the context.
* @param int $id id of the enrol_lti_tools instance (i.e. the id of the 'published resource').
* @param string $uuid a v4 uuid identifier for this published resource.
* @param bool $supportsgrades whether or not this resource supports grades.
* @param float|null $grademax the max grade or null if this is not a graded resource.
* @param bool $iscourse whether or not this resource is itself a course.
*/
public function __construct(string $name, string $coursefullname, int $courseid, int $contextid, int $id,
string $uuid, bool $supportsgrades, ?float $grademax, bool $iscourse) {
$this->name = $name;
$this->coursefullname = $coursefullname;
$this->courseid = $courseid;
$this->contextid = $contextid;
$this->id = $id;
$this->uuid = $uuid;
$this->supportsgrades = $supportsgrades;
$this->grademax = $grademax;
$this->iscourse = $iscourse;
}
/**
* Get the name of this published resource.
*
* @return string the localised name.
*/
public function get_name(): string {
return $this->name;
}
/**
* Get the full name of the course owning this published resource.
*
* @return string the localised course full name.
*/
public function get_coursefullname(): string {
return $this->coursefullname;
}
/**
* Get the id of the course owning this published resource.
*
* @return int the course id.
*/
public function get_courseid(): int {
return $this->courseid;
}
/**
* Get the id of the context for this published resource.
*
* @return int the context id.
*/
public function get_contextid(): int {
return $this->contextid;
}
/**
* Get the id of this published resource.
*
* @return int the id.
*/
public function get_id(): int {
return $this->id;
}
/**
* Get the uuid for this published resource.
*
* @return string v4 uuid.
*/
public function get_uuid(): string {
return $this->uuid;
}
/**
* Check whether this resource supports grades or not.
*
* @return bool true if supported, false otherwise.
*/
public function supports_grades(): bool {
return $this->supportsgrades;
}
/**
* Get the max grade for this published resource, if its a graded resource.
*
* @return float|null the grade max, if grades are supported, else null.
*/
public function get_grademax(): ?float {
return $this->grademax;
}
/**
* Check whether this published resource is a course itself.
*
* @return bool true if it's a course, false otherwise.
*/
public function is_course(): bool {
return $this->iscourse;
}
}
+286
View File
@@ -0,0 +1,286 @@
<?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/>.
/**
* Displays enrolment LTI instances.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_lti;
defined('MOODLE_INTERNAL') || die;
global $CFG;
require_once($CFG->libdir . '/tablelib.php');
/**
* Handles displaying enrolment LTI instances.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manage_table extends \table_sql {
/**
* @var \enrol_plugin $ltiplugin
*/
protected $ltiplugin;
/**
* @var bool $ltienabled
*/
protected $ltienabled;
/**
* @var bool $canconfig
*/
protected $canconfig;
/**
* @var int $courseid The course id.
*/
protected $courseid;
/**
* Sets up the table.
*
* @param string $courseid The id of the course.
*/
public function __construct($courseid) {
parent::__construct('enrol_lti_manage_table');
$this->define_columns(array(
'name',
'launch',
'registration',
'edit'
));
$this->define_headers(array(
get_string('name'),
get_string('launchdetails', 'enrol_lti'),
get_string('registrationurl', 'enrol_lti'),
get_string('edit')
));
$this->collapsible(false);
$this->sortable(false);
// Set the variables we need access to.
$this->ltiplugin = enrol_get_plugin('lti');
$this->ltienabled = enrol_is_enabled('lti');
$this->canconfig = has_capability('moodle/course:enrolconfig', \context_course::instance($courseid));
$this->courseid = $courseid;
// Set help icons.
$launchicon = new \help_icon('launchdetails', 'enrol_lti');
$regicon = new \help_icon('registrationurl', 'enrol_lti');
$this->define_help_for_headers(['1' => $launchicon, '2' => $regicon]);
}
/**
* Generate the name column.
*
* @param \stdClass $tool event data.
* @return string
*/
public function col_name($tool) {
$toolcontext = \context::instance_by_id($tool->contextid, IGNORE_MISSING);
$name = $toolcontext ? helper::get_name($tool) : $this->get_deleted_activity_name_html($tool);
return $this->get_display_text($tool, $name);
}
/**
* Generate the launch column.
*
* @param \stdClass $tool instance data.
* @return string
*/
public function col_launch($tool) {
global $OUTPUT;
$url = helper::get_cartridge_url($tool);
$cartridgeurllabel = get_string('cartridgeurl', 'enrol_lti');
$cartridgeurl = $url;
$secretlabel = get_string('secret', 'enrol_lti');
$secret = $tool->secret;
$launchurl = helper::get_launch_url($tool->id);
$launchurllabel = get_string('launchurl', 'enrol_lti');
$data = [
"rows" => [
[ "label" => $cartridgeurllabel, "text" => $cartridgeurl, "id" => "cartridgeurl", "hidelabel" => false ],
[ "label" => $secretlabel, "text" => $secret, "id" => "secret", "hidelabel" => false ],
[ "label" => $launchurllabel, "text" => $launchurl, "id" => "launchurl", "hidelabel" => false ],
]
];
$return = $OUTPUT->render_from_template("enrol_lti/copy_grid", $data);
return $return;
}
/**
* Generate the Registration column.
*
* @param \stdClass $tool instance data.
* @return string
*/
public function col_registration($tool) {
global $OUTPUT;
$url = helper::get_proxy_url($tool);
$toolurllabel = get_string("registrationurl", "enrol_lti");
$toolurl = $url;
$data = [
"rows" => [
[ "label" => $toolurllabel, "text" => $toolurl, "id" => "toolurl" , "hidelabel" => true],
]
];
$return = $OUTPUT->render_from_template("enrol_lti/copy_grid", $data);
return $return;
}
/**
* Generate the edit column.
*
* @param \stdClass $tool event data.
* @return string
*/
public function col_edit($tool) {
global $OUTPUT;
$buttons = array();
$instance = new \stdClass();
$instance->id = $tool->enrolid;
$instance->courseid = $tool->courseid;
$instance->enrol = 'lti';
$instance->status = $tool->status;
$strdelete = get_string('delete');
$strenable = get_string('enable');
$strdisable = get_string('disable');
$url = new \moodle_url('/enrol/lti/index.php',
array('sesskey' => sesskey(), 'courseid' => $this->courseid, 'legacy' => 1));
if ($this->ltiplugin->can_delete_instance($instance)) {
$aurl = new \moodle_url($url, array('action' => 'delete', 'instanceid' => $instance->id));
$buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/delete', $strdelete, 'core',
array('class' => 'iconsmall')));
}
if ($this->ltienabled && $this->ltiplugin->can_hide_show_instance($instance)) {
if ($instance->status == ENROL_INSTANCE_ENABLED) {
$aurl = new \moodle_url($url, array('action' => 'disable', 'instanceid' => $instance->id));
$buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/hide', $strdisable, 'core',
array('class' => 'iconsmall')));
} else if ($instance->status == ENROL_INSTANCE_DISABLED) {
$aurl = new \moodle_url($url, array('action' => 'enable', 'instanceid' => $instance->id));
$buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/show', $strenable, 'core',
array('class' => 'iconsmall')));
}
}
if ($this->ltienabled && $this->canconfig) {
$linkparams = array(
'courseid' => $instance->courseid,
'id' => $instance->id,
'type' => $instance->enrol,
'legacy' => 1,
'returnurl' => new \moodle_url('/enrol/lti/index.php',
array('courseid' => $this->courseid, 'legacy' => 1))
);
$editlink = new \moodle_url("/enrol/editinstance.php", $linkparams);
$buttons[] = $OUTPUT->action_icon($editlink, new \pix_icon('t/edit', get_string('edit'), 'core',
array('class' => 'iconsmall')));
}
return implode(' ', $buttons);
}
/**
* Query the reader. Store results in the object for use by build_table.
*
* @param int $pagesize size of page for paginated displayed table.
* @param bool $useinitialsbar do you want to use the initials bar.
*/
public function query_db($pagesize, $useinitialsbar = true) {
$total = \enrol_lti\helper::count_lti_tools(['courseid' => $this->courseid, 'ltiversion' => 'LTI-1p0/LTI-2p0']);
$this->pagesize($pagesize, $total);
$tools = \enrol_lti\helper::get_lti_tools(['courseid' => $this->courseid, 'ltiversion' => 'LTI-1p0/LTI-2p0'],
$this->get_page_start(), $this->get_page_size());
$this->rawdata = $tools;
// Set initial bars.
if ($useinitialsbar) {
$this->initialbars($total > $pagesize);
}
}
/**
* Returns text to display in the columns.
*
* @param \stdClass $tool the tool
* @param string $text the text to alter
* @return string
*/
protected function get_display_text($tool, $text) {
if ($tool->status != ENROL_INSTANCE_ENABLED) {
return \html_writer::tag('div', $text, array('class' => 'dimmed_text'));
}
return $text;
}
/**
* Get a warning icon, with tooltip, describing enrolment instances sharing activities which have been deleted.
*
* @param \stdClass $tool the tool instance record.
* @return string the HTML for the name column.
*/
protected function get_deleted_activity_name_html(\stdClass $tool): string {
global $OUTPUT;
$icon = \html_writer::tag(
'a',
$OUTPUT->pix_icon('enrolinstancewarning', get_string('deletedactivityalt' , 'enrol_lti'), 'enrol_lti'), [
"class" => "btn btn-link p-0",
"role" => "button",
"data-container" => "body",
"data-toggle" => "popover",
"data-placement" => right_to_left() ? "left" : "right",
"data-content" => get_string('deletedactivitydescription', 'enrol_lti'),
"data-html" => "true",
"tabindex" => "0",
"data-trigger" => "focus"
]
);
$name = \html_writer::span($icon . get_string('deletedactivity', 'enrol_lti'));
if ($tool->name) {
$name .= \html_writer::empty_tag('br') . \html_writer::empty_tag('br') . $tool->name;
}
return $name;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?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/>.
/**
* Tool registration page class.
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_lti\output;
defined('MOODLE_INTERNAL') || die;
use renderable;
use renderer_base;
use templatable;
use stdClass;
/**
* Tool registration page class.
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class registration implements renderable, templatable {
/** @var returnurl The url to which the tool proxy should return */
protected $returnurl;
/**
* Construct a new tool registration page
* @param string|null $returnurl The url the consumer wants us to return the user to (optional)
*/
public function __construct($returnurl = null) {
$this->returnurl = $returnurl;
}
/**
* Export the data.
*
* @param renderer_base $output
* @return stdClass Data to be used for the template
*/
public function export_for_template(renderer_base $output) {
$data = new stdClass();
$data->returnurl = $this->returnurl;
return $data;
}
}
+281
View File
@@ -0,0 +1,281 @@
<?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/>.
/**
* Renderer class for LTI enrolment
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_lti\output;
defined('MOODLE_INTERNAL') || die();
use core\output\notification;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use Packback\Lti1p3\LtiMessageLaunch;
use plugin_renderer_base;
/**
* Renderer class for LTI enrolment
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends plugin_renderer_base {
/**
* Render the enrol_lti/proxy_registration template
*
* @param registration $registration The registration renderable
* @return string html for the page
*/
public function render_registration(registration $registration) {
$data = $registration->export_for_template($this);
return parent::render_from_template("enrol_lti/proxy_registration", $data);
}
/**
* Render the content item selection (deep linking 2.0) view
*
* This view is a form containing a list of courses and modules which, once selected and submitted, will result in
* a list of LTI Resource Link Content Items being sent back to the platform, allowing resource link creation to
* take place.
*
* @param LtiMessageLaunch $launch the launch data.
* @param array $resources array of published resources available to the current user.
* @return string html
*/
public function render_published_resource_selection_view(LtiMessageLaunch $launch, array $resources): string {
global $CFG;
$context = [
'action' => $CFG->wwwroot . '/enrol/lti/configure.php',
'launchid' => $launch->getLaunchId(),
'hascontent' => !empty($resources),
'sesskey' => sesskey(),
'courses' => []
];
foreach ($resources as $resource) {
$context['courses'][$resource->get_courseid()]['fullname'] = $resource->get_coursefullname();
if (!$resource->is_course()) {
$context['courses'][$resource->get_courseid()]['modules'][] = [
'name' => $resource->get_name(),
'id' => $resource->get_id(),
'lineitem' => $resource->supports_grades()
];
if (empty($context['courses'][$resource->get_courseid()]['shared_course'])) {
$context['courses'][$resource->get_courseid()]['shared_course'] = false;
}
} else {
$context['courses'][$resource->get_courseid()]['shared_course'] = $resource->is_course();
$context['courses'][$resource->get_courseid()]['id'] = $resource->get_id();
$context['courses'][$resource->get_courseid()]['lineitem'] = $resource->supports_grades();
}
}
$context['courses'] = array_values($context['courses']); // Reset keys for use in the template.
return parent::render_from_template('enrol_lti/local/ltiadvantage/content_select', $context);
}
/**
* Render the table applications which have been registered as LTI Advantage platforms.
*
* @param array $registrations The list of registrations to render.
* @return string the html.
*/
public function render_admin_setting_registered_platforms(array $registrations): string {
$registrationscontext = [
'registrations' => [],
'addurl' => (new \moodle_url('/enrol/lti/register_platform.php', ['action' => 'add']))->out(false),
];
$registrationscontext['hasregs'] = count($registrations) > 0;
$deploymentrepository = new deployment_repository();
foreach ($registrations as $reg) {
$countdeployments = $deploymentrepository->count_by_registration($reg->get_id());
$status = get_string('registrationstatuspending', 'enrol_lti');
if ($reg->is_complete()) {
$status = get_string('registrationstatusactive', 'enrol_lti');
}
$registrationscontext['registrations'][] = [
'name' => $reg->get_name(),
'issuer' => $reg->get_platformid(),
'clientid' => $reg->get_clientid(),
'hasdeployments' => $countdeployments > 0,
'countdeployments' => $countdeployments,
'isactive' => $reg->is_complete(),
'statusstring' => $status,
'tooldetailsurl' => (new \moodle_url('/enrol/lti/register_platform.php',
['action' => 'view', 'regid' => $reg->get_id(), 'tabselect' => 'tooldetails']))->out(false),
'platformdetailsurl' => (new \moodle_url('/enrol/lti/register_platform.php',
['action' => 'view', 'regid' => $reg->get_id(), 'tabselect' => 'platformdetails']))->out(false),
'deploymentsurl' => (new \moodle_url('/enrol/lti/register_platform.php',
['action' => 'view', 'regid' => $reg->get_id(), 'tabselect' => 'tooldeployments']))->out(false),
'deleteurl' => (new \moodle_url('/enrol/lti/register_platform.php',
['action' => 'delete', 'regid' => $reg->get_id()]))->out(false)
];
}
// Notice to let users know this is LTI Advantage ONLY.
$versionnotice = new notification(
get_string('registeredplatformsltiversionnotice', 'enrol_lti'),
notification::NOTIFY_INFO
);
$versionnotice->set_show_closebutton(false);
$return = parent::render($versionnotice);
$return .= parent::render_from_template('enrol_lti/local/ltiadvantage/registered_platforms',
$registrationscontext);
return $return;
}
/**
* Renders the registration view page, allowing admins to view tool details, platform details and deployments.
*
* The template uses dynamic tabs, which renders with one active tab and uses js to change tabs if desired. E.g. if an anchor
* link is used to go to another tab, the page will first load the active tab, then switch to the tab referenced in the anchor
* using JS. To allow navigation to the page with a specific tab selected, and WITHOUT the js slowdown, this renderer method
* allows callers to specify which tab is set as the active tab during first render.
* Valid values correspond to the tab names in the enrol_lti/local/ltiadvantage/registration_view template, currently:
* - 'tooldetails' - to render with the Tool details tab as the active tab
* - 'platformdetails' - to render with the Platform details tab as the active tab
* - 'tooldeployments' - to render with the Tool deployments tab as the active tab
* By default, the platformdetails tab will be selected as active.
*
* @param int $registrationid the id of the registration to display information for.
* @param string $activetab a string identifying the tab to preselect when rendering.
* @return bool|string
* @throws \coding_exception
* @throws \moodle_exception
*/
public function render_registration_view(int $registrationid, string $activetab = '') {
global $CFG;
$validtabvals = ['tooldetails', 'platformdetails', 'tooldeployments'];
$activetab = !empty($activetab) && in_array($activetab, $validtabvals) ? $activetab : 'platformdetails';
$regrepo = new application_registration_repository();
$registration = $regrepo->find($registrationid);
$deploymentrepo = new deployment_repository();
$deployments = $deploymentrepo->find_all_by_registration($registration->get_id());
$deploymentscontext = [];
foreach ($deployments as $deployment) {
$deploymentscontext[] = [
'name' => $deployment->get_deploymentname(),
'deploymentid' => $deployment->get_deploymentid(),
'deleteurl' => (new \moodle_url(
'/enrol/lti/manage_deployment.php',
['action' => 'delete', 'id' => $deployment->get_id(), 'registrationid' => $registration->get_id()]
))->out(false)
];
}
$regurl = new \moodle_url('/enrol/lti/register.php', ['token' => $registration->get_uniqueid()]);
$tcontext = [
'tool_details_active' => $activetab == 'tooldetails',
'platform_details_active' => $activetab == 'platformdetails',
'tool_deployments_active' => $activetab == 'tooldeployments',
'back_url' => (new \moodle_url('/admin/settings.php', ['section' => 'enrolsettingslti_registrations']))->out(false),
'dynamic_registration_info' => get_string(
'registrationurlinfomessage',
'enrol_lti',
get_docs_url('Publish_as_LTI_tool')
),
'dynamic_registration_url' => [
'name' => get_string('registrationurl', 'enrol_lti'),
'url' => $regurl,
'id' => uniqid()
],
'manual_registration_info' => get_string('endpointltiversionnotice', 'enrol_lti'),
'manual_registration_urls' => [
[
'name' => get_string('toolurl', 'enrol_lti'),
'url' => $CFG->wwwroot . '/enrol/lti/launch.php',
'id' => uniqid()
],
[
'name' => get_string('loginurl', 'enrol_lti'),
'url' => $CFG->wwwroot . '/enrol/lti/login.php?id=' . $registration->get_uniqueid(),
'id' => uniqid()
],
[
'name' => get_string('jwksurl', 'enrol_lti'),
'url' => $CFG->wwwroot . '/enrol/lti/jwks.php',
'id' => uniqid()
],
[
'name' => get_string('deeplinkingurl', 'enrol_lti'),
'url' => $CFG->wwwroot . '/enrol/lti/launch_deeplink.php',
'id' => uniqid()
],
],
'platform_details_info' => get_string('platformdetailsinfo', 'enrol_lti'),
'platform_details' => [
[
'name' => get_string('registerplatform:name', 'enrol_lti'),
'value' => $registration->get_name()
],
[
'name' => get_string('registerplatform:platformid', 'enrol_lti'),
'value' => $registration->get_platformid() ?? '',
],
[
'name' => get_string('registerplatform:clientid', 'enrol_lti'),
'value' => $registration->get_clientid() ?? '',
],
[
'name' => get_string('registerplatform:authrequesturl', 'enrol_lti'),
'value' => $registration->get_authenticationrequesturl() ?? '',
],
[
'name' => get_string('registerplatform:jwksurl', 'enrol_lti'),
'value' => $registration->get_jwksurl() ?? '',
],
[
'name' => get_string('registerplatform:accesstokenurl', 'enrol_lti'),
'value' => $registration->get_accesstokenurl() ?? '',
]
],
'edit_platform_details_url' => (new \moodle_url('/enrol/lti/register_platform.php',
['action' => 'edit', 'regid' => $registration->get_id()]))->out(false),
'deployments_info' => get_string('deploymentsinfo', 'enrol_lti'),
'has_deployments' => !empty($deploymentscontext),
'tool_deployments' => $deploymentscontext,
'add_deployment_url' => (new \moodle_url('/enrol/lti/manage_deployment.php',
['action' => 'add', 'registrationid' => $registrationid]))->out(false)
];
return parent::render_from_template('enrol_lti/local/ltiadvantage/registration_view',
$tcontext);
}
/**
* Render a warning, indicating to the user that cookies are require but couldn't be set.
*
* @return string the html.
*/
public function render_cookies_required_notice(): string {
$notification = new notification(get_string('cookiesarerequiredinfo', 'enrol_lti'), notification::NOTIFY_WARNING, false);
$tcontext = [
'heading' => get_string('cookiesarerequired', 'enrol_lti'),
'notification' => $notification->export_for_template($this),
];
return parent::render_from_template('enrol_lti/local/ltiadvantage/cookies_required_notice', $tcontext);
}
}
+250
View File
@@ -0,0 +1,250 @@
<?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/>.
/**
* Privacy Subsystem implementation for enrol_lti.
*
* @package enrol_lti
* @category privacy
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_lti\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for enrol_lti.
*
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\plugin\provider,
\core_privacy\local\request\core_userlist_provider {
/**
* Return the fields which contain personal data.
*
* @param collection $items a reference to the collection to use to store the metadata.
* @return collection the updated collection of metadata items.
*/
public static function get_metadata(collection $items): collection {
$items->add_database_table(
'enrol_lti_users',
[
'userid' => 'privacy:metadata:enrol_lti_users:userid',
'lastgrade' => 'privacy:metadata:enrol_lti_users:lastgrade',
'lastaccess' => 'privacy:metadata:enrol_lti_users:lastaccess',
'timecreated' => 'privacy:metadata:enrol_lti_users:timecreated'
],
'privacy:metadata:enrol_lti_users'
);
return $items;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$contextlist = new contextlist();
$sql = "SELECT DISTINCT ctx.id
FROM {enrol_lti_users} ltiusers
JOIN {enrol_lti_tools} ltitools
ON ltiusers.toolid = ltitools.id
JOIN {context} ctx
ON ctx.id = ltitools.contextid
WHERE ltiusers.userid = :userid";
$params = ['userid' => $userid];
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
if (!($context instanceof \context_course || $context instanceof \context_module)) {
return;
}
$sql = "SELECT ltiusers.userid
FROM {enrol_lti_users} ltiusers
JOIN {enrol_lti_tools} ltitools ON ltiusers.toolid = ltitools.id
WHERE ltitools.contextid = :contextid";
$params = ['contextid' => $context->id];
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
if (empty($contextlist->count())) {
return;
}
$user = $contextlist->get_user();
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$sql = "SELECT ltiusers.lastgrade, ltiusers.lastaccess, ltiusers.timecreated, ltitools.contextid
FROM {enrol_lti_users} ltiusers
JOIN {enrol_lti_tools} ltitools
ON ltiusers.toolid = ltitools.id
JOIN {context} ctx
ON ctx.id = ltitools.contextid
WHERE ctx.id {$contextsql}
AND ltiusers.userid = :userid";
$params = $contextparams + ['userid' => $user->id];
$ltiusers = $DB->get_recordset_sql($sql, $params);
self::recordset_loop_and_export($ltiusers, 'contextid', [], function($carry, $record) {
$carry[] = [
'lastgrade' => $record->lastgrade,
'timecreated' => transform::datetime($record->lastaccess),
'timemodified' => transform::datetime($record->timecreated)
];
return $carry;
}, function($contextid, $data) {
$context = \context::instance_by_id($contextid);
$finaldata = (object) $data;
writer::with_context($context)->export_data(['enrol_lti_users'], $finaldata);
});
}
/**
* Delete all user data which matches the specified context.
*
* @param \context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
if (!($context instanceof \context_course || $context instanceof \context_module)) {
return;
}
$enrolltitools = $DB->get_fieldset_select('enrol_lti_tools', 'id', 'contextid = :contextid',
['contextid' => $context->id]);
if (!empty($enrolltitools)) {
list($sql, $params) = $DB->get_in_or_equal($enrolltitools, SQL_PARAMS_NAMED);
$DB->delete_records_select('enrol_lti_users', 'toolid ' . $sql, $params);
}
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
global $DB;
$userid = $contextlist->get_user()->id;
foreach ($contextlist->get_contexts() as $context) {
if (!($context instanceof \context_course || $context instanceof \context_module)) {
continue;
}
$enrolltitools = $DB->get_fieldset_select('enrol_lti_tools', 'id', 'contextid = :contextid',
['contextid' => $context->id]);
if (!empty($enrolltitools)) {
list($sql, $params) = $DB->get_in_or_equal($enrolltitools, SQL_PARAMS_NAMED);
$params = array_merge($params, ['userid' => $userid]);
$DB->delete_records_select('enrol_lti_users', "toolid $sql AND userid = :userid", $params);
}
}
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
$context = $userlist->get_context();
if (!($context instanceof \context_course || $context instanceof \context_module)) {
return;
}
$enrolltitools = $DB->get_fieldset_select('enrol_lti_tools', 'id', 'contextid = :contextid',
['contextid' => $context->id]);
if (!empty($enrolltitools)) {
list($toolsql, $toolparams) = $DB->get_in_or_equal($enrolltitools, SQL_PARAMS_NAMED);
$userids = $userlist->get_userids();
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params = $toolparams + $userparams;
$DB->delete_records_select('enrol_lti_users', "toolid $toolsql AND userid $usersql", $params);
}
}
/**
* Loop and export from a recordset.
*
* @param \moodle_recordset $recordset The recordset.
* @param string $splitkey The record key to determine when to export.
* @param mixed $initial The initial data to reduce from.
* @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
* @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
* @return void
*/
protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
callable $reducer, callable $export) {
$data = $initial;
$lastid = null;
foreach ($recordset as $record) {
if ($lastid && $record->{$splitkey} != $lastid) {
$export($lastid, $data);
$data = $initial;
}
$data = $reducer($data, $record);
$lastid = $record->{$splitkey};
}
$recordset->close();
if (!empty($lastid)) {
$export($lastid, $data);
}
}
}
+193
View File
@@ -0,0 +1,193 @@
<?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/>.
/**
* Handles synchronising grades for the enrolment LTI.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_lti\task;
/**
* Task for synchronising grades for the enrolment LTI.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sync_grades extends \core\task\scheduled_task {
/**
* Get a descriptive name for this task.
*
* @return string
*/
public function get_name() {
return get_string('tasksyncgrades', 'enrol_lti');
}
/**
* Performs the synchronisation of grades.
*
* @return bool|void
*/
public function execute() {
global $DB, $CFG;
require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuth.php');
require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuthBody.php');
require_once($CFG->dirroot . '/lib/completionlib.php');
require_once($CFG->libdir . '/gradelib.php');
require_once($CFG->dirroot . '/grade/querylib.php');
// Check if the authentication plugin is disabled.
if (!is_enabled_auth('lti')) {
mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')));
return true;
}
// Check if the enrolment plugin is disabled - isn't really necessary as the task should not run if
// the plugin is disabled, but there is no harm in making sure core hasn't done something wrong.
if (!enrol_is_enabled('lti')) {
mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'));
return true;
}
// Get all the enabled tools.
if ($tools = \enrol_lti\helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED, 'gradesync' => 1,
'ltiversion' => 'LTI-1p0/LTI-2p0'))) {
foreach ($tools as $tool) {
mtrace("Starting - Grade sync for shared tool '$tool->id' for the course '$tool->courseid'.");
// Variables to keep track of information to display later.
$usercount = 0;
$sendcount = 0;
// We check for all the users - users can access the same tool from different consumers.
if ($ltiusers = $DB->get_records('enrol_lti_users', array('toolid' => $tool->id), 'lastaccess DESC')) {
$completion = new \completion_info(get_course($tool->courseid));
foreach ($ltiusers as $ltiuser) {
$mtracecontent = "for the user '$ltiuser->userid' in the tool '$tool->id' for the course " .
"'$tool->courseid'";
$usercount = $usercount + 1;
// Check if we do not have a serviceurl - this can happen if the sync process has an unexpected error.
if (empty($ltiuser->serviceurl)) {
mtrace("Skipping - Empty serviceurl $mtracecontent.");
continue;
}
// Check if we do not have a sourceid - this can happen if the sync process has an unexpected error.
if (empty($ltiuser->sourceid)) {
mtrace("Skipping - Empty sourceid $mtracecontent.");
continue;
}
// Need a valid context to continue.
if (!$context = \context::instance_by_id($tool->contextid, IGNORE_MISSING)) {
mtrace("Failed - Invalid contextid '$tool->contextid' for the tool '$tool->id'.");
continue;
}
// Ok, let's get the grade.
$grade = false;
if ($context->contextlevel == CONTEXT_COURSE) {
// Check if the user did not completed the course when it was required.
if ($tool->gradesynccompletion && !$completion->is_course_complete($ltiuser->userid)) {
mtrace("Skipping - Course not completed $mtracecontent.");
continue;
}
// Get the grade.
if ($grade = grade_get_course_grade($ltiuser->userid, $tool->courseid)) {
$grademax = floatval($grade->item->grademax);
$grade = $grade->grade;
}
} else if ($context->contextlevel == CONTEXT_MODULE) {
$cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
if ($tool->gradesynccompletion) {
$data = $completion->get_data($cm, false, $ltiuser->userid);
if ($data->completionstate != COMPLETION_COMPLETE_PASS &&
$data->completionstate != COMPLETION_COMPLETE) {
mtrace("Skipping - Activity not completed $mtracecontent.");
continue;
}
}
$grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance, $ltiuser->userid);
if (!empty($grades->items[0]->grades)) {
$grade = reset($grades->items[0]->grades);
if (!empty($grade->item)) {
$grademax = floatval($grade->item->grademax);
} else {
$grademax = floatval($grades->items[0]->grademax);
}
$grade = $grade->grade;
}
}
if ($grade === false || $grade === null || strlen($grade) < 1) {
mtrace("Skipping - Invalid grade $mtracecontent.");
continue;
}
// No need to be dividing by zero.
if (empty($grademax)) {
mtrace("Skipping - Invalid grade $mtracecontent.");
continue;
}
// Check to see if the grade has changed.
if (!grade_floats_different($grade, $ltiuser->lastgrade)) {
mtrace("Not sent - The grade $mtracecontent was not sent as the grades are the same.");
continue;
}
// Sync with the external system.
$floatgrade = $grade / $grademax;
$body = \enrol_lti\helper::create_service_body($ltiuser->sourceid, $floatgrade);
try {
$response = sendOAuthBodyPOST('POST', $ltiuser->serviceurl,
$ltiuser->consumerkey, $ltiuser->consumersecret, 'application/xml', $body);
} catch (\Exception $e) {
mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
mtrace($e->getMessage());
continue;
}
if (strpos(strtolower($response), 'success') !== false) {
$DB->set_field('enrol_lti_users', 'lastgrade', grade_floatval($grade), array('id' => $ltiuser->id));
mtrace("Success - The grade '$floatgrade' $mtracecontent was sent.");
$sendcount = $sendcount + 1;
} else {
mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
}
}
}
mtrace("Completed - Synced grades for tool '$tool->id' in the course '$tool->courseid'. " .
"Processed $usercount users; sent $sendcount grades.");
mtrace("");
}
}
}
}
+463
View File
@@ -0,0 +1,463 @@
<?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/>.
/**
* Handles synchronising members using the enrolment LTI.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_lti\task;
defined('MOODLE_INTERNAL') || die();
use core\task\scheduled_task;
use core_user;
use enrol_lti\data_connector;
use enrol_lti\helper;
use IMSGlobal\LTI\ToolProvider\Context;
use IMSGlobal\LTI\ToolProvider\ResourceLink;
use IMSGlobal\LTI\ToolProvider\ToolConsumer;
use IMSGlobal\LTI\ToolProvider\User;
use stdClass;
require_once($CFG->dirroot . '/user/lib.php');
/**
* Task for synchronising members using the enrolment LTI.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sync_members extends scheduled_task {
/** @var array Array of user photos. */
protected $userphotos = [];
/** @var data_connector $dataconnector A data_connector instance. */
protected $dataconnector;
/**
* Get a descriptive name for this task.
*
* @return string
*/
public function get_name() {
return get_string('tasksyncmembers', 'enrol_lti');
}
/**
* Performs the synchronisation of members.
*/
public function execute() {
if (!is_enabled_auth('lti')) {
mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')));
return;
}
// Check if the enrolment plugin is disabled - isn't really necessary as the task should not run if
// the plugin is disabled, but there is no harm in making sure core hasn't done something wrong.
if (!enrol_is_enabled('lti')) {
mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'));
return;
}
$this->dataconnector = new data_connector();
// Get all the enabled tools.
$tools = helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED, 'membersync' => 1,
'ltiversion' => 'LTI-1p0/LTI-2p0'));
foreach ($tools as $tool) {
mtrace("Starting - Member sync for published tool '$tool->id' for course '$tool->courseid'.");
// Variables to keep track of information to display later.
$usercount = 0;
$enrolcount = 0;
$unenrolcount = 0;
// Fetch consumer records mapped to this tool.
$consumers = $this->dataconnector->get_consumers_mapped_to_tool($tool->id);
// Perform processing for each consumer.
foreach ($consumers as $consumer) {
mtrace("Requesting membership service for the tool consumer '{$consumer->getRecordId()}'");
// Get members through this tool consumer.
$members = $this->fetch_members_from_consumer($consumer);
// Check if we were able to fetch the members.
if ($members === false) {
mtrace("Skipping - Membership service request failed.\n");
continue;
}
// Fetched members count.
$membercount = count($members);
$usercount += $membercount;
mtrace("$membercount members received.\n");
// Process member information.
list($users, $enrolledcount) = $this->sync_member_information($tool, $consumer, $members);
$enrolcount += $enrolledcount;
// Now sync unenrolments for the consumer.
$unenrolcount += $this->sync_unenrol($tool, $consumer->getKey(), $users);
}
mtrace("Completed - Synced members for tool '$tool->id' in the course '$tool->courseid'. " .
"Processed $usercount users; enrolled $enrolcount members; unenrolled $unenrolcount members.\n");
}
// Sync the user profile photos.
mtrace("Started - Syncing user profile images.");
$countsyncedimages = $this->sync_profile_images();
mtrace("Completed - Synced $countsyncedimages profile images.");
}
/**
* Fetches the members that belong to a ToolConsumer.
*
* @param ToolConsumer $consumer
* @return bool|User[]
*/
protected function fetch_members_from_consumer(ToolConsumer $consumer) {
$dataconnector = $this->dataconnector;
// Get membership URL template from consumer profile data.
$defaultmembershipsurl = null;
if (isset($consumer->profile->service_offered)) {
$servicesoffered = $consumer->profile->service_offered;
foreach ($servicesoffered as $service) {
if (isset($service->{'@id'}) && strpos($service->{'@id'}, 'tcp:ToolProxyBindingMemberships') !== false &&
isset($service->endpoint)) {
$defaultmembershipsurl = $service->endpoint;
if (isset($consumer->profile->product_instance->product_info->product_family->vendor->code)) {
$vendorcode = $consumer->profile->product_instance->product_info->product_family->vendor->code;
$defaultmembershipsurl = str_replace('{vendor_code}', $vendorcode, $defaultmembershipsurl);
}
$defaultmembershipsurl = str_replace('{product_code}', $consumer->getKey(), $defaultmembershipsurl);
break;
}
}
}
$members = false;
// Fetch the resource link linked to the consumer.
$resourcelink = $dataconnector->get_resourcelink_from_consumer($consumer);
if ($resourcelink !== null) {
// Try to perform a membership service request using this resource link.
$members = $this->do_resourcelink_membership_request($resourcelink);
}
// If membership service can't be performed through resource link, fallback through context memberships.
if ($members === false) {
// Fetch context records that are mapped to this ToolConsumer.
$contexts = $dataconnector->get_contexts_from_consumer($consumer);
// Perform membership service request for each of these contexts.
foreach ($contexts as $context) {
$contextmembership = $this->do_context_membership_request($context, $resourcelink, $defaultmembershipsurl);
if ($contextmembership) {
// Add $contextmembership contents to $members array.
if (is_array($members)) {
$members = array_merge($members, $contextmembership);
} else {
$members = $contextmembership;
}
}
}
}
return $members;
}
/**
* Method to determine whether to sync unenrolments or not.
*
* @param int $syncmode The tool's membersyncmode.
* @return bool
*/
protected function should_sync_unenrol($syncmode) {
return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_UNENROL_MISSING;
}
/**
* Method to determine whether to sync enrolments or not.
*
* @param int $syncmode The tool's membersyncmode.
* @return bool
*/
protected function should_sync_enrol($syncmode) {
return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_ENROL_NEW;
}
/**
* Performs synchronisation of member information and enrolments.
*
* @param stdClass $tool
* @param ToolConsumer $consumer
* @param User[] $members
* @return array An array of users from processed members and the number that were enrolled.
*/
protected function sync_member_information(stdClass $tool, ToolConsumer $consumer, $members) {
global $DB;
$users = [];
$enrolcount = 0;
// Process member information.
foreach ($members as $member) {
// Set the user data.
$user = new stdClass();
$user->username = helper::create_username($consumer->getKey(), $member->ltiUserId);
$user->firstname = core_user::clean_field($member->firstname, 'firstname');
$user->lastname = core_user::clean_field($member->lastname, 'lastname');
$user->email = core_user::clean_field($member->email, 'email');
// Get the user data from the LTI consumer.
$user = helper::assign_user_tool_data($tool, $user);
$dbuser = core_user::get_user_by_username($user->username, 'id');
if ($dbuser) {
// If email is empty remove it, so we don't update the user with an empty email.
if (empty($user->email)) {
unset($user->email);
}
$user->id = $dbuser->id;
user_update_user($user);
// Add the information to the necessary arrays.
$users[$user->id] = $user;
$this->userphotos[$user->id] = $member->image;
} else {
if ($this->should_sync_enrol($tool->membersyncmode)) {
// If the email was stripped/not set then fill it with a default one. This
// stops the user from being redirected to edit their profile page.
if (empty($user->email)) {
$user->email = $user->username . "@example.com";
}
$user->auth = 'lti';
$user->id = user_create_user($user);
// Add the information to the necessary arrays.
$users[$user->id] = $user;
$this->userphotos[$user->id] = $member->image;
}
}
// Sync enrolments.
if ($this->should_sync_enrol($tool->membersyncmode)) {
// Enrol the user in the course.
if (helper::enrol_user($tool, $user->id) === helper::ENROLMENT_SUCCESSFUL) {
// Increment enrol count.
$enrolcount++;
}
// Check if this user has already been registered in the enrol_lti_users table.
if (!$DB->record_exists('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) {
// Create an initial enrol_lti_user record that we can use later when syncing grades and members.
$userlog = new stdClass();
$userlog->userid = $user->id;
$userlog->toolid = $tool->id;
$userlog->consumerkey = $consumer->getKey();
$DB->insert_record('enrol_lti_users', $userlog);
}
}
}
return [$users, $enrolcount];
}
/**
* Performs unenrolment of users that are no longer enrolled in the consumer side.
*
* @param stdClass $tool The tool record object.
* @param string $consumerkey ensure we only unenrol users from this tool consumer.
* @param array $currentusers The list of current users.
* @return int The number of users that have been unenrolled.
*/
protected function sync_unenrol(stdClass $tool, string $consumerkey, array $currentusers) {
global $DB;
$ltiplugin = enrol_get_plugin('lti');
if (!$this->should_sync_unenrol($tool->membersyncmode)) {
return 0;
}
if (empty($currentusers)) {
return 0;
}
$unenrolcount = 0;
$select = "toolid = :toolid AND " . $DB->sql_compare_text('consumerkey', 255) . " = :consumerkey";
$ltiusersrs = $DB->get_recordset_select('enrol_lti_users', $select, ['toolid' => $tool->id, 'consumerkey' => $consumerkey],
'lastaccess DESC', 'userid');
// Go through the users and check if any were never listed, if so, remove them.
foreach ($ltiusersrs as $ltiuser) {
if (!array_key_exists($ltiuser->userid, $currentusers)) {
$instance = new stdClass();
$instance->id = $tool->enrolid;
$instance->courseid = $tool->courseid;
$instance->enrol = 'lti';
$ltiplugin->unenrol_user($instance, $ltiuser->userid);
// Increment unenrol count.
$unenrolcount++;
}
}
$ltiusersrs->close();
return $unenrolcount;
}
/**
* Performs synchronisation of user profile images.
*/
protected function sync_profile_images() {
$counter = 0;
foreach ($this->userphotos as $userid => $url) {
if ($url) {
$result = helper::update_user_profile_image($userid, $url);
if ($result === helper::PROFILE_IMAGE_UPDATE_SUCCESSFUL) {
$counter++;
mtrace("Profile image succesfully downloaded and created for user '$userid' from $url.");
} else {
mtrace($result);
}
}
}
return $counter;
}
/**
* Performs membership service request using an LTI Context object.
*
* If the context has a 'custom_context_memberships_url' setting, we use this to perform the membership service request.
* Otherwise, if a context is associated with resource link, we try first to get the members using the
* ResourceLink::doMembershipsService() method.
* If we're still unable to fetch members from the resource link, we try to build a memberships URL from the memberships URL
* endpoint template that is defined in the ToolConsumer profile and substitute the parameters accordingly.
*
* @param Context $context The context object.
* @param ResourceLink $resourcelink The resource link object.
* @param string $membershipsurltemplate The memberships endpoint URL template.
* @return bool|User[] Array of User objects upon successful membership service request. False, otherwise.
*/
protected function do_context_membership_request(Context $context, ResourceLink $resourcelink = null,
$membershipsurltemplate = '') {
$dataconnector = $this->dataconnector;
// Flag to indicate whether to save the context later.
$contextupdated = false;
// If membership URL is not set, try to generate using the default membership URL from the consumer profile.
if (!$context->hasMembershipService()) {
if (empty($membershipsurltemplate)) {
mtrace("Skipping - No membership service available.\n");
return false;
}
if ($resourcelink === null) {
$resourcelink = $dataconnector->get_resourcelink_from_context($context);
}
if ($resourcelink !== null) {
// Try to perform a membership service request using this resource link.
$resourcelinkmembers = $this->do_resourcelink_membership_request($resourcelink);
if ($resourcelinkmembers) {
// If we're able to fetch members using this resource link, return these.
return $resourcelinkmembers;
}
}
// If fetching memberships through resource link failed and we don't have a memberships URL, build one from template.
mtrace("'custom_context_memberships_url' not set. Fetching default template: $membershipsurltemplate");
$membershipsurl = $membershipsurltemplate;
// Check if we need to fetch tool code.
$needstoolcode = strpos($membershipsurl, '{tool_code}') !== false;
if ($needstoolcode) {
$toolcode = false;
// Fetch tool code from the resource link data.
$lisresultsourcedidjson = $resourcelink->getSetting('lis_result_sourcedid');
if ($lisresultsourcedidjson) {
$lisresultsourcedid = json_decode($lisresultsourcedidjson);
if (isset($lisresultsourcedid->data->typeid)) {
$toolcode = $lisresultsourcedid->data->typeid;
}
}
if ($toolcode) {
// Substitute fetched tool code value.
$membershipsurl = str_replace('{tool_code}', $toolcode, $membershipsurl);
} else {
// We're unable to determine the tool code. End this processing.
return false;
}
}
// Get context_id parameter and substitute, if applicable.
$membershipsurl = str_replace('{context_id}', $context->getId(), $membershipsurl);
// Get context_type and substitute, if applicable.
if (strpos($membershipsurl, '{context_type}') !== false) {
$contexttype = $context->type !== null ? $context->type : 'CourseSection';
$membershipsurl = str_replace('{context_type}', $contexttype, $membershipsurl);
}
// Save this URL for the context's custom_context_memberships_url setting.
$context->setSetting('custom_context_memberships_url', $membershipsurl);
$contextupdated = true;
}
// Perform membership service request.
$url = $context->getSetting('custom_context_memberships_url');
mtrace("Performing membership service request from context with URL {$url}.");
$members = $context->getMembership();
// Save the context if membership request succeeded and if it has been updated.
if ($members && $contextupdated) {
$context->save();
}
return $members;
}
/**
* Performs membership service request using ResourceLink::doMembershipsService() method.
*
* @param ResourceLink $resourcelink
* @return bool|User[] Array of User objects upon successful membership service request. False, otherwise.
*/
protected function do_resourcelink_membership_request(ResourceLink $resourcelink) {
$members = false;
$membershipsurl = $resourcelink->getSetting('ext_ims_lis_memberships_url');
$membershipsid = $resourcelink->getSetting('ext_ims_lis_memberships_id');
if ($membershipsurl && $membershipsid) {
mtrace("Performing membership service request from resource link with membership URL: " . $membershipsurl);
$members = $resourcelink->doMembershipsService(true);
}
return $members;
}
}
+455
View File
@@ -0,0 +1,455 @@
<?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/>.
/**
* Extends the IMS Tool provider library for the LTI enrolment.
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_lti;
defined('MOODLE_INTERNAL') || die;
use context;
use core\notification;
use core_user;
use enrol_lti\output\registration;
use html_writer;
use IMSGlobal\LTI\Profile\Item;
use IMSGlobal\LTI\Profile\Message;
use IMSGlobal\LTI\Profile\ResourceHandler;
use IMSGlobal\LTI\Profile\ServiceDefinition;
use IMSGlobal\LTI\ToolProvider\ToolProvider;
use moodle_exception;
use moodle_url;
use stdClass;
require_once($CFG->dirroot . '/user/lib.php');
/**
* Extends the IMS Tool provider library for the LTI enrolment.
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_provider extends ToolProvider {
/**
* @var stdClass $tool The object representing the enrol instance providing this LTI tool
*/
protected $tool;
/**
* Remove $this->baseUrl (wwwroot) from a given url string and return it.
*
* @param string $url The url from which to remove the base url
* @return string|null A string of the relative path to the url, or null if it couldn't be determined.
*/
protected function strip_base_url($url) {
if (substr($url, 0, strlen($this->baseUrl)) == $this->baseUrl) {
return substr($url, strlen($this->baseUrl));
}
return null;
}
/**
* Create a new instance of tool_provider to handle all the LTI tool provider interactions.
*
* @param int $toolid The id of the tool to be provided.
*/
public function __construct($toolid) {
global $CFG, $SITE;
$token = helper::generate_proxy_token($toolid);
$tool = helper::get_lti_tool($toolid);
$this->tool = $tool;
$dataconnector = new data_connector();
parent::__construct($dataconnector);
// Override debugMode and set to the configured value.
$this->debugMode = $CFG->debugdeveloper;
$this->baseUrl = $CFG->wwwroot;
$toolpath = helper::get_launch_url($toolid);
$toolpath = $this->strip_base_url($toolpath);
$vendorid = $SITE->shortname;
$vendorname = $SITE->fullname;
$vendordescription = trim(html_to_text($SITE->summary));
$this->vendor = new Item($vendorid, $vendorname, $vendordescription, $CFG->wwwroot);
$name = helper::get_name($tool);
$description = helper::get_description($tool);
$icon = helper::get_icon($tool)->out();
$icon = $this->strip_base_url($icon);
$this->product = new Item(
$token,
$name,
$description,
helper::get_proxy_url($tool),
'1.0'
);
$requiredmessages = [
new Message(
'basic-lti-launch-request',
$toolpath,
[
'Context.id',
'CourseSection.title',
'CourseSection.label',
'CourseSection.sourcedId',
'CourseSection.longDescription',
'CourseSection.timeFrame.begin',
'ResourceLink.id',
'ResourceLink.title',
'ResourceLink.description',
'User.id',
'User.username',
'Person.name.full',
'Person.name.given',
'Person.name.family',
'Person.email.primary',
'Person.sourcedId',
'Person.name.middle',
'Person.address.street1',
'Person.address.locality',
'Person.address.country',
'Person.address.timezone',
'Person.phone.primary',
'Person.phone.mobile',
'Person.webaddress',
'Membership.role',
'Result.sourcedId',
'Result.autocreate'
]
)
];
$optionalmessages = [
];
$this->resourceHandlers[] = new ResourceHandler(
new Item(
$token,
helper::get_name($tool),
$description
),
$icon,
$requiredmessages,
$optionalmessages
);
$this->requiredServices[] = new ServiceDefinition(['application/vnd.ims.lti.v2.toolproxy+json'], ['POST']);
$this->requiredServices[] = new ServiceDefinition(['application/vnd.ims.lis.v2.membershipcontainer+json'], ['GET']);
}
/**
* Override onError for custom error handling.
* @return void
*/
protected function onError() {
global $OUTPUT;
$message = $this->message;
if ($this->debugMode && !empty($this->reason)) {
$message = $this->reason;
}
// Display the error message from the provider's side if the consumer has not specified a URL to pass the error to.
if (empty($this->returnUrl)) {
$this->errorOutput = $OUTPUT->notification(get_string('failedrequest', 'enrol_lti', ['reason' => $message]), 'error');
}
}
/**
* Override onLaunch with tool logic.
* @return void
*/
protected function onLaunch() {
global $DB, $SESSION, $CFG;
// Check for valid consumer.
if (empty($this->consumer) || $this->dataConnector->loadToolConsumer($this->consumer) === false) {
$this->ok = false;
$this->message = get_string('invalidtoolconsumer', 'enrol_lti');
return;
}
$url = helper::get_launch_url($this->tool->id);
// If a tool proxy has been stored for the current consumer trying to access a tool,
// check that the tool is being launched from the correct url.
$correctlaunchurl = false;
if (!empty($this->consumer->toolProxy)) {
$proxy = json_decode($this->consumer->toolProxy);
$handlers = $proxy->tool_profile->resource_handler;
foreach ($handlers as $handler) {
foreach ($handler->message as $message) {
$handlerurl = new moodle_url($message->path);
$fullpath = $handlerurl->out(false);
if ($message->message_type == "basic-lti-launch-request" && $fullpath == $url) {
$correctlaunchurl = true;
break 2;
}
}
}
} else if ($this->tool->secret == $this->consumer->secret) {
// Test if the LTI1 secret for this tool is being used. Then we know the correct tool is being launched.
$correctlaunchurl = true;
}
if (!$correctlaunchurl) {
$this->ok = false;
$this->message = get_string('invalidrequest', 'enrol_lti');
return;
}
// Before we do anything check that the context is valid.
$tool = $this->tool;
$context = context::instance_by_id($tool->contextid);
// Set the user data.
$user = new stdClass();
$user->username = helper::create_username($this->consumer->getKey(), $this->user->ltiUserId);
if (!empty($this->user->firstname)) {
$user->firstname = $this->user->firstname;
} else {
$user->firstname = $this->user->getRecordId();
}
if (!empty($this->user->lastname)) {
$user->lastname = $this->user->lastname;
} else {
$user->lastname = $this->tool->contextid;
}
$user->email = core_user::clean_field($this->user->email, 'email');
// Get the user data from the LTI consumer.
$user = helper::assign_user_tool_data($tool, $user);
// Check if the user exists.
if (!$dbuser = $DB->get_record('user', ['username' => $user->username, 'deleted' => 0])) {
// If the email was stripped/not set then fill it with a default one. This
// stops the user from being redirected to edit their profile page.
if (empty($user->email)) {
$user->email = $user->username . "@example.com";
}
$user->auth = 'lti';
$user->id = \user_create_user($user);
// Get the updated user record.
$user = $DB->get_record('user', ['id' => $user->id]);
} else {
if (helper::user_match($user, $dbuser)) {
$user = $dbuser;
} else {
// If email is empty remove it, so we don't update the user with an empty email.
if (empty($user->email)) {
unset($user->email);
}
$user->id = $dbuser->id;
\user_update_user($user);
// Get the updated user record.
$user = $DB->get_record('user', ['id' => $user->id]);
}
}
// Update user image.
if (isset($this->user) && isset($this->user->image) && !empty($this->user->image)) {
$image = $this->user->image;
} else {
// Use custom_user_image parameter as a fallback.
$image = $this->resourceLink->getSetting('custom_user_image');
}
// Check if there is an image to process.
if ($image) {
helper::update_user_profile_image($user->id, $image);
}
// Check if we need to force the page layout to embedded.
$isforceembed = $this->resourceLink->getSetting('custom_force_embed') == 1;
// Check if we are an instructor.
$isinstructor = $this->user->isStaff() || $this->user->isAdmin();
if ($context->contextlevel == CONTEXT_COURSE) {
$courseid = $context->instanceid;
$urltogo = new moodle_url('/course/view.php', ['id' => $courseid]);
} else if ($context->contextlevel == CONTEXT_MODULE) {
$cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
$urltogo = new moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]);
// If we are a student in the course module context we do not want to display blocks.
if (!$isforceembed && !$isinstructor) {
$isforceembed = true;
}
} else {
throw new \moodle_exception('invalidcontext');
exit();
}
// Force page layout to embedded if necessary.
if ($isforceembed) {
$SESSION->forcepagelayout = 'embedded';
} else {
// May still be set from previous session, so unset it.
unset($SESSION->forcepagelayout);
}
// Enrol the user in the course with no role.
$result = helper::enrol_user($tool, $user->id);
// Display an error, if there is one.
if ($result !== helper::ENROLMENT_SUCCESSFUL) {
throw new \moodle_exception($result, 'enrol_lti');
exit();
}
// Give the user the role in the given context.
$roleid = $isinstructor ? $tool->roleinstructor : $tool->rolelearner;
role_assign($roleid, $user->id, $tool->contextid);
// Login user.
$sourceid = $this->user->ltiResultSourcedId;
$serviceurl = $this->resourceLink->getSetting('lis_outcome_service_url');
// Check if we have recorded this user before.
if ($userlog = $DB->get_record('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) {
if ($userlog->sourceid != $sourceid) {
$userlog->sourceid = $sourceid;
}
if ($userlog->serviceurl != $serviceurl) {
$userlog->serviceurl = $serviceurl;
}
if (empty($userlog->consumersecret)) {
$userlog->consumersecret = $this->consumer->secret;
}
$userlog->lastaccess = time();
$DB->update_record('enrol_lti_users', $userlog);
} else {
// Add the user details so we can use it later when syncing grades and members.
$userlog = new stdClass();
$userlog->userid = $user->id;
$userlog->toolid = $tool->id;
$userlog->serviceurl = $serviceurl;
$userlog->sourceid = $sourceid;
$userlog->consumerkey = $this->consumer->getKey();
$userlog->consumersecret = $this->consumer->secret;
$userlog->lastgrade = 0;
$userlog->lastaccess = time();
$userlog->timecreated = time();
$userlog->membershipsurl = $this->resourceLink->getSetting('ext_ims_lis_memberships_url');
$userlog->membershipsid = $this->resourceLink->getSetting('ext_ims_lis_memberships_id');
$DB->insert_record('enrol_lti_users', $userlog);
}
// Finalise the user log in.
complete_user_login($user);
// Everything's good. Set appropriate OK flag and message values.
$this->ok = true;
$this->message = get_string('success');
if (empty($CFG->allowframembedding)) {
// Provide an alternative link.
$stropentool = get_string('opentool', 'enrol_lti');
echo html_writer::tag('p', get_string('frameembeddingnotenabled', 'enrol_lti'));
echo html_writer::link($urltogo, $stropentool, ['target' => '_blank']);
} else {
// All done, redirect the user to where they want to go.
redirect($urltogo);
}
}
/**
* Override onRegister with registration code.
*/
protected function onRegister() {
global $PAGE;
if (empty($this->consumer)) {
$this->ok = false;
$this->message = get_string('invalidtoolconsumer', 'enrol_lti');
return;
}
if (empty($this->returnUrl)) {
$this->ok = false;
$this->message = get_string('returnurlnotset', 'enrol_lti');
return;
}
if ($this->doToolProxyService()) {
// Map tool consumer and published tool, if necessary.
$this->map_tool_to_consumer();
// Indicate successful processing in message.
$this->message = get_string('successfulregistration', 'enrol_lti');
// Prepare response.
$returnurl = new moodle_url($this->returnUrl);
$returnurl->param('lti_msg', get_string("successfulregistration", "enrol_lti"));
$returnurl->param('status', 'success');
$guid = $this->consumer->getKey();
$returnurl->param('tool_proxy_guid', $guid);
$returnurlout = $returnurl->out(false);
$registration = new registration($returnurlout);
$output = $PAGE->get_renderer('enrol_lti');
echo $output->render($registration);
} else {
// Tell the consumer that the registration failed.
$this->ok = false;
$this->message = get_string('couldnotestablishproxy', 'enrol_lti');
}
}
/**
* Performs mapping of the tool consumer to a published tool.
*
* @throws moodle_exception
*/
public function map_tool_to_consumer() {
global $DB;
if (empty($this->consumer)) {
throw new moodle_exception('invalidtoolconsumer', 'enrol_lti');
}
// Map the consumer to the tool.
$mappingparams = [
'toolid' => $this->tool->id,
'consumerid' => $this->consumer->getRecordId()
];
$mappingexists = $DB->record_exists('enrol_lti_tool_consumer_map', $mappingparams);
if (!$mappingexists) {
$DB->insert_record('enrol_lti_tool_consumer_map', (object) $mappingparams);
}
}
}
+103
View File
@@ -0,0 +1,103 @@
<?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/>.
/**
* Returns the deep link resource via a POST to the platform.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core\http_client;
use enrol_lti\local\ltiadvantage\lib\lti_cookie;
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
use enrol_lti\local\ltiadvantage\lib\issuer_database;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\published_resource_repository;
use Packback\Lti1p3\DeepLinkResources\Resource;
use Packback\Lti1p3\LtiConstants;
use Packback\Lti1p3\LtiLineitem;
use Packback\Lti1p3\LtiMessageLaunch;
use Packback\Lti1p3\LtiServiceConnector;
require(__DIR__.'/../../config.php');
require_once(__DIR__.'/lib.php');
global $CFG, $DB, $PAGE, $USER;
require_once($CFG->libdir . '/filelib.php');
require_login(null, false);
require_sesskey();
$launchid = required_param('launchid', PARAM_TEXT);
$modules = optional_param_array('modules', [], PARAM_INT);
$grades = optional_param_array('grades', [], PARAM_INT);
$sesscache = new launch_cache_session();
$issdb = new issuer_database(new application_registration_repository(), new deployment_repository());
$cookie = new lti_cookie();
$serviceconnector = new LtiServiceConnector($sesscache, new http_client());
$messagelaunch = LtiMessageLaunch::fromCache($launchid, $issdb, $sesscache, $cookie, $serviceconnector);
if (!$messagelaunch->isDeepLinkLaunch()) {
throw new coding_exception('Configuration can only be accessed as part of a content item selection deep link '.
'launch.');
}
$sesscache->purge();
// Get the selected resources and create the resource link content items to post back.
$resourcerepo = new published_resource_repository();
$resources = $resourcerepo->find_all_by_ids_for_user($modules, $USER->id);
$contentitems = [];
foreach ($resources as $resource) {
$contentitem = Resource::new()
->setUrl($CFG->wwwroot . '/enrol/lti/launch.php')
->setCustomParams(['id' => $resource->get_uuid()])
->setTitle($resource->get_name());
// If the activity supports grading, and the user has selected it, then include line item information.
if ($resource->supports_grades() && in_array($resource->get_id(), $grades)) {
require_once($CFG->libdir . '/gradelib.php');
$lineitem = LtiLineitem::new()
->setScoreMaximum($resource->get_grademax())
->setResourceId($resource->get_uuid());
$contentitem->setLineitem($lineitem);
}
$contentitems[] = $contentitem;
}
global $USER, $CFG, $OUTPUT;
$PAGE->set_context(context_system::instance());
$url = new moodle_url('/enrol/lti/configure.php');
$PAGE->set_url($url);
$PAGE->set_pagelayout('popup');
echo $OUTPUT->header();
$dl = $messagelaunch->getDeepLink();
$formactionurl = $messagelaunch->getLaunchData()[LtiConstants::DL_DEEP_LINK_SETTINGS]['deep_link_return_url'];
echo <<<HTML
<form id="auto_submit" action="{$formactionurl}" method="POST">
<input type="hidden" name="JWT" value="{$messagelaunch->getDeepLink()->getResponseJwt($contentitems)}" />
<input type="submit" name="Go" />
</form>
<script>document.getElementById('auto_submit').submit();</script>
HTML;
+47
View File
@@ -0,0 +1,47 @@
<?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/>.
/**
* Capabilities for LTI enrolment plugin.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = array(
/* Add, edit or remove lti enrol instance. */
'enrol/lti:config' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
)
),
'enrol/lti:unenrol' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
)
),
);
+40
View File
@@ -0,0 +1,40 @@
<?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/>.
/**
* Post installation code for enrol_lti.
*
* @package enrol_lti
* @copyright 2022 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Stub for database installation.
*/
function xmldb_enrol_lti_install() {
global $CFG, $OUTPUT;
// LTI 1.3: Set a private key for this site (which is acting as a tool in LTI 1.3).
require_once($CFG->dirroot . '/enrol/lti/upgradelib.php');
$warning = enrol_lti_verify_private_key();
if (!empty($warning)) {
echo $OUTPUT->notification($warning, 'notifyproblem');
}
}
+295
View File
@@ -0,0 +1,295 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="enrol/lti/db" VERSION="20220211" COMMENT="XMLDB file for Moodle enrol/lti"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="enrol_lti_tools" COMMENT="List of tools provided to the remote system">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="enrolid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="ltiversion" TYPE="char" LENGTH="15" NOTNULL="true" DEFAULT="LTI-1p3" SEQUENCE="false"/>
<FIELD NAME="institution" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="lang" TYPE="char" LENGTH="30" NOTNULL="true" DEFAULT="en" SEQUENCE="false"/>
<FIELD NAME="timezone" TYPE="char" LENGTH="100" NOTNULL="true" DEFAULT="99" SEQUENCE="false"/>
<FIELD NAME="maxenrolled" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="maildisplay" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="2" SEQUENCE="false"/>
<FIELD NAME="city" TYPE="char" LENGTH="120" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="country" TYPE="char" LENGTH="2" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="gradesync" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="gradesynccompletion" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="membersync" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="membersyncmode" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="roleinstructor" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="rolelearner" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="secret" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="uuid" TYPE="char" LENGTH="36" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="provisioningmodelearner" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="provisioningmodeinstructor" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="enrolid" TYPE="foreign" FIELDS="enrolid" REFTABLE="enrol" REFFIELDS="id"/>
<KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
<KEY NAME="uuid" TYPE="unique" FIELDS="uuid"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_users" COMMENT="User access log and gradeback data">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="toolid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="serviceurl" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="sourceid" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="ltideploymentid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="consumerkey" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="consumersecret" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="membershipsurl" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="membershipsid" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="lastgrade" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="The last grade that was sent"/>
<FIELD NAME="lastaccess" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The time the user last accessed"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The time the user was created"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="toolid" TYPE="foreign" FIELDS="toolid" REFTABLE="enrol_lti_tools" REFFIELDS="id"/>
<KEY NAME="ltideploymentid" TYPE="foreign" FIELDS="ltideploymentid" REFTABLE="enrol_lti_deployment" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_lti2_consumer" COMMENT="LTI consumers interacting with moodle">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="name" TYPE="char" LENGTH="50" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="consumerkey256" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="consumerkey" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="secret" TYPE="char" LENGTH="1024" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="ltiversion" TYPE="char" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="consumername" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="consumerversion" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="consumerguid" TYPE="char" LENGTH="1024" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="profile" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="toolproxy" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="settings" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="protected" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="enablefrom" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="enableuntil" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="lastaccess" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="created" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="updated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="consumerkey256_uniq" UNIQUE="true" FIELDS="consumerkey256"/>
</INDEXES>
</TABLE>
<TABLE NAME="enrol_lti_lti2_tool_proxy" COMMENT="A tool proxy between moodle and a consumer">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="toolproxykey" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="consumerid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="toolproxy" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="created" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="updated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="toolproxykey_uniq" TYPE="unique" FIELDS="toolproxykey"/>
<KEY NAME="consumerid" TYPE="foreign" FIELDS="consumerid" REFTABLE="enrol_lti_lti2_consumer" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_lti2_context" COMMENT="Information about a specific LTI contexts from the consumers">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="consumerid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="lticontextkey" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="type" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="settings" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="created" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="updated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="consumerid" TYPE="foreign" FIELDS="consumerid" REFTABLE="enrol_lti_lti2_consumer" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_lti2_nonce" COMMENT="Nonce used for authentication between moodle and a consumer">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="consumerid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="value" TYPE="char" LENGTH="64" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="expires" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="consumerid" TYPE="foreign" FIELDS="consumerid" REFTABLE="enrol_lti_lti2_consumer" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_lti2_resource_link" COMMENT="Link from the consumer to the tool">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="11" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="consumerid" TYPE="int" LENGTH="11" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="ltiresourcelinkkey" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="settings" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="primaryresourcelinkid" TYPE="int" LENGTH="11" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="shareapproved" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="created" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="updated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="enrol_lti_lti2_context" REFFIELDS="id"/>
<KEY NAME="primaryresourcelinkid" TYPE="foreign" FIELDS="primaryresourcelinkid" REFTABLE="enrol_lti_lti2_resource_link" REFFIELDS="id"/>
<KEY NAME="consumerid" TYPE="foreign" FIELDS="consumerid" REFTABLE="enrol_lti_lti2_consumer" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_lti2_share_key" COMMENT="Resource link share key">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="sharekey" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="resourcelinkid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="autoapprove" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="expires" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="sharekey" TYPE="unique" FIELDS="sharekey"/>
<KEY NAME="resourcelinkid" TYPE="foreign-unique" FIELDS="resourcelinkid" REFTABLE="enrol_lti_lti2_resource_link" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_lti2_user_result" COMMENT="Results for each user for each resource link">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="resourcelinkid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="ltiuserkey" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="ltiresultsourcedid" TYPE="char" LENGTH="1024" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="created" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="updated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="resourcelinkid" TYPE="foreign" FIELDS="resourcelinkid" REFTABLE="enrol_lti_lti2_resource_link" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_tool_consumer_map" COMMENT="Table that maps the published tool to tool consumers.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="toolid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false" COMMENT="The tool ID."/>
<FIELD NAME="consumerid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false" COMMENT="The consumer ID."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="toolid" TYPE="foreign" FIELDS="toolid" REFTABLE="enrol_lti_tools" REFFIELDS="id"/>
<KEY NAME="consumerid" TYPE="foreign" FIELDS="consumerid" REFTABLE="enrol_lti_lti2_consumer" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="enrol_lti_app_registration" COMMENT="Details of each application that has been registered with the tool">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Common name to identify this platform to users"/>
<FIELD NAME="platformid" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The issuer URL"/>
<FIELD NAME="clientid" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="The clientid string, generated by the platform when setting up the tool."/>
<FIELD NAME="uniqueid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="A unique local id, which can be used in the initiate login URI to provide {iss, clientid} uniqueness in the absence of the optional client_id claim."/>
<FIELD NAME="platformclienthash" TYPE="char" LENGTH="64" NOTNULL="false" SEQUENCE="false" COMMENT="SHA256 hash of the platformid (issuer) and clientid"/>
<FIELD NAME="platformuniqueidhash" TYPE="char" LENGTH="64" NOTNULL="false" SEQUENCE="false" COMMENT="SHA256 hash of the platformid (issuer) and uniqueid"/>
<FIELD NAME="authenticationrequesturl" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The authorisation endpoint of the platform"/>
<FIELD NAME="jwksurl" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The JSON Web Key Set URL for the platform"/>
<FIELD NAME="accesstokenurl" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="status" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Status of the registration, used to denote draft (incomplete) or active (complete)"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="uniqueid" TYPE="unique" FIELDS="uniqueid"/>
</KEYS>
<INDEXES>
<INDEX NAME="platformclienthash" UNIQUE="true" FIELDS="platformclienthash"/>
<INDEX NAME="platformuniqueidhash" UNIQUE="true" FIELDS="platformuniqueidhash"/>
</INDEXES>
</TABLE>
<TABLE NAME="enrol_lti_deployment" COMMENT="Each row represents a deployment of a tool within a platform.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="A short name identifying the tool deployment to users"/>
<FIELD NAME="deploymentid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the deployment, as defined in the platform"/>
<FIELD NAME="platformid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The platformid to which this deployment belongs"/>
<FIELD NAME="legacyconsumerkey" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="The legacy consumer key mapped to this deployment, if the deployment represents a migrated tool."/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="platformid" TYPE="foreign" FIELDS="platformid" REFTABLE="enrol_lti_app_registration" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="platformid-deploymentid" UNIQUE="true" FIELDS="platformid, deploymentid"/>
</INDEXES>
</TABLE>
<TABLE NAME="enrol_lti_resource_link" COMMENT="Each row represents a resource link for a platform and deployment">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="resourcelinkid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The platform-and-deployment-unique id of the resource link"/>
<FIELD NAME="ltideploymentid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the enrol_lti_deployment record containing the deployment information."/>
<FIELD NAME="resourceid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the local enrol_lti_tools record containing information about the published resource to which this resource link relates."/>
<FIELD NAME="lticontextid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The id of the enrol_lti_context record containing information about the context from which this resource link originates."/>
<FIELD NAME="lineitemsservice" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="The URL for the line items service for this resource link"/>
<FIELD NAME="lineitemservice" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="The URL for the line item service (if only one line item present)."/>
<FIELD NAME="lineitemscope" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="The ags line items authorization scope"/>
<FIELD NAME="resultscope" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="The ags result authorization scope"/>
<FIELD NAME="scorescope" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="The ags score items authorization scope"/>
<FIELD NAME="contextmembershipsurl" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="The NRPS membership URL"/>
<FIELD NAME="nrpsserviceversions" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="The NRPS supported service versions"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="ltideploymentid" TYPE="foreign" FIELDS="ltideploymentid" REFTABLE="enrol_lti_deployment" REFFIELDS="id"/>
<KEY NAME="lticontextid" TYPE="foreign" FIELDS="lticontextid" REFTABLE="enrol_lti_context" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="resourcelinkid-ltideploymentid" UNIQUE="true" FIELDS="resourcelinkid, ltideploymentid"/>
</INDEXES>
</TABLE>
<TABLE NAME="enrol_lti_context" COMMENT="Each row represents a context in the platform, where resource links are added within a deployment.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="contextid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the context on the platform"/>
<FIELD NAME="ltideploymentid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the enrol_lti_deployment record containing the deployment information."/>
<FIELD NAME="type" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The type of the context on the platform"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="ltideploymentid" TYPE="foreign" FIELDS="ltideploymentid" REFTABLE="enrol_lti_deployment" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="ltideploymentid-contextid" UNIQUE="true" FIELDS="ltideploymentid, contextid"/>
</INDEXES>
</TABLE>
<TABLE NAME="enrol_lti_user_resource_link" COMMENT="Join table mapping users to resource links as this is a many:many relationship">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="ltiuserid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the enrol_lti_users record"/>
<FIELD NAME="resourcelinkid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the enrol_lti_resource_link record."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="ltiuserid" TYPE="foreign" FIELDS="ltiuserid" REFTABLE="enrol_lti_users" REFFIELDS="id"/>
<KEY NAME="resourcelinkid" TYPE="foreign" FIELDS="resourcelinkid" REFTABLE="enrol_lti_resource_link" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="ltiuserid-resourcelinkid" UNIQUE="true" FIELDS="ltiuserid, resourcelinkid"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
+62
View File
@@ -0,0 +1,62 @@
<?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/>.
/**
* Enrol LTI tasks.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$tasks = array(
array(
'classname' => 'enrol_lti\task\sync_grades',
'blocking' => 0,
'minute' => '*/30',
'hour' => '*',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
),
array(
'classname' => 'enrol_lti\task\sync_members',
'blocking' => 0,
'minute' => '*/30',
'hour' => '*',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
),
array(
'classname' => 'enrol_lti\local\ltiadvantage\task\sync_members',
'blocking' => 0,
'minute' => '*/30',
'hour' => '*',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
),
array(
'classname' => 'enrol_lti\local\ltiadvantage\task\sync_grades',
'blocking' => 0,
'minute' => '*/30',
'hour' => '*',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
),
);
+51
View File
@@ -0,0 +1,51 @@
<?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 keeps track of upgrades to the lti enrolment plugin
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* xmldb_lti_upgrade is the function that upgrades
* the lti module database when is needed
*
* This function is automaticly called when version number in
* version.php changes.
*
* @param int $oldversion New old version number.
*
* @return boolean
*/
function xmldb_enrol_lti_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
+22
View File
@@ -0,0 +1,22 @@
The MIT License
Copyright (c) 2007 Andy Smith
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+813
View File
@@ -0,0 +1,813 @@
<?php
// vim: foldmethod=marker
$OAuth_last_computed_siguature = false;
/* Generic exception class
*/
class OAuthException extends \Exception {
// pass
}
class OAuthConsumer {
public $key;
public $secret;
/** @var string|null To store callback_url. */
protected $callback_url;
function __construct($key, $secret, $callback_url=NULL) {
$this->key = $key;
$this->secret = $secret;
$this->callback_url = $callback_url;
}
function __toString() {
return "OAuthConsumer[key=$this->key,secret=$this->secret]";
}
}
class OAuthToken {
// access tokens and request tokens
public $key;
public $secret;
/**
* key = the token
* secret = the token secret
*/
function __construct($key, $secret) {
$this->key = $key;
$this->secret = $secret;
}
/**
* generates the basic string serialization of a token that a server
* would respond to request_token and access_token calls with
*/
function to_string() {
return "oauth_token=" .
OAuthUtil::urlencode_rfc3986($this->key) .
"&oauth_token_secret=" .
OAuthUtil::urlencode_rfc3986($this->secret);
}
function __toString() {
return $this->to_string();
}
}
class OAuthSignatureMethod {
public function check_signature(&$request, $consumer, $token, $signature) {
$built = $this->build_signature($request, $consumer, $token);
return $built == $signature;
}
}
class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
function get_name() {
return "HMAC-SHA1";
}
public function build_signature($request, $consumer, $token) {
global $OAuth_last_computed_signature;
$OAuth_last_computed_signature = false;
$base_string = $request->get_signature_base_string();
$request->base_string = $base_string;
$key_parts = array(
$consumer->secret,
($token) ? $token->secret : ""
);
$key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
$key = implode('&', $key_parts);
$computed_signature = base64_encode(hash_hmac('sha1', $base_string, $key, true));
$OAuth_last_computed_signature = $computed_signature;
return $computed_signature;
}
}
class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
public function get_name() {
return "PLAINTEXT";
}
public function build_signature($request, $consumer, $token) {
$sig = array(
OAuthUtil::urlencode_rfc3986($consumer->secret)
);
if ($token) {
array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret));
} else {
array_push($sig, '');
}
$raw = implode("&", $sig);
// for debug purposes
$request->base_string = $raw;
return OAuthUtil::urlencode_rfc3986($raw);
}
}
class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
public function get_name() {
return "RSA-SHA1";
}
protected function fetch_public_cert(&$request) {
// not implemented yet, ideas are:
// (1) do a lookup in a table of trusted certs keyed off of consumer
// (2) fetch via http using a url provided by the requester
// (3) some sort of specific discovery code based on request
//
// either way should return a string representation of the certificate
throw Exception("fetch_public_cert not implemented");
}
protected function fetch_private_cert(&$request) {
// not implemented yet, ideas are:
// (1) do a lookup in a table of trusted certs keyed off of consumer
//
// either way should return a string representation of the certificate
throw Exception("fetch_private_cert not implemented");
}
public function build_signature(&$request, $consumer, $token) {
$base_string = $request->get_signature_base_string();
$request->base_string = $base_string;
// Fetch the private key cert based on the request
$cert = $this->fetch_private_cert($request);
// Pull the private key ID from the certificate
$privatekeyid = openssl_get_privatekey($cert);
// Sign using the key
$ok = openssl_sign($base_string, $signature, $privatekeyid);
// Avoid passing null values to base64_encode.
if (!$ok) {
throw new OAuthException("OpenSSL unable to sign data");
}
// TODO: Remove this block once PHP 8.0 becomes required.
if (PHP_MAJOR_VERSION < 8) {
// Release the key resource
openssl_free_key($privatekeyid);
}
return base64_encode($signature);
}
public function check_signature(&$request, $consumer, $token, $signature) {
$decoded_sig = base64_decode($signature);
$base_string = $request->get_signature_base_string();
// Fetch the public key cert based on the request
$cert = $this->fetch_public_cert($request);
// Pull the public key ID from the certificate
$publickeyid = openssl_get_publickey($cert);
// Check the computed signature against the one passed in the query
$ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
// TODO: Remove this block once PHP 8.0 becomes required.
if (PHP_MAJOR_VERSION < 8) {
// Release the key resource
openssl_free_key($publickeyid);
}
return $ok == 1;
}
}
class OAuthRequest {
private $parameters;
private $http_method;
private $http_url;
// for debug purposes
public $base_string;
public static $version = '1.0';
public static $POST_INPUT = 'php://input';
function __construct($http_method, $http_url, $parameters=NULL) {
@$parameters or $parameters = array();
$this->parameters = $parameters;
$this->http_method = $http_method;
$this->http_url = $http_url;
}
/**
* attempt to build up a request from what was passed to the server
*/
public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
$scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
? 'http'
: 'https';
$port = "";
if ( $_SERVER['SERVER_PORT'] != "80" && $_SERVER['SERVER_PORT'] != "443" &&
strpos(':', $_SERVER['HTTP_HOST']) < 0 ) {
$port = ':' . $_SERVER['SERVER_PORT'] ;
}
@$http_url or $http_url = $scheme .
'://' . $_SERVER['HTTP_HOST'] .
$port .
$_SERVER['REQUEST_URI'];
@$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
// We weren't handed any parameters, so let's find the ones relevant to
// this request.
// If you run XML-RPC or similar you should use this to provide your own
// parsed parameter-list
if (!$parameters) {
// Find request headers
$request_headers = OAuthUtil::get_headers();
// Parse the query-string to find GET parameters
$parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
$ourpost = $_POST;
// Add POST Parameters if they exist
$parameters = array_merge($parameters, $ourpost);
// We have a Authorization-header with OAuth data. Parse the header
// and add those overriding any duplicates from GET or POST
if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
$header_parameters = OAuthUtil::split_header(
$request_headers['Authorization']
);
$parameters = array_merge($parameters, $header_parameters);
}
}
return new OAuthRequest($http_method, $http_url, $parameters);
}
/**
* pretty much a helper function to set up the request
*/
public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
@$parameters or $parameters = array();
$defaults = array("oauth_version" => OAuthRequest::$version,
"oauth_nonce" => OAuthRequest::generate_nonce(),
"oauth_timestamp" => OAuthRequest::generate_timestamp(),
"oauth_consumer_key" => $consumer->key);
if ($token)
$defaults['oauth_token'] = $token->key;
$parameters = array_merge($defaults, $parameters);
// Parse the query-string to find and add GET parameters
$parts = parse_url($http_url);
if ( !empty($parts['query']) ) {
$qparms = OAuthUtil::parse_parameters($parts['query']);
$parameters = array_merge($qparms, $parameters);
}
return new OAuthRequest($http_method, $http_url, $parameters);
}
public function set_parameter($name, $value, $allow_duplicates = true) {
if ($allow_duplicates && isset($this->parameters[$name])) {
// We have already added parameter(s) with this name, so add to the list
if (is_scalar($this->parameters[$name])) {
// This is the first duplicate, so transform scalar (string)
// into an array so we can add the duplicates
$this->parameters[$name] = array($this->parameters[$name]);
}
$this->parameters[$name][] = $value;
} else {
$this->parameters[$name] = $value;
}
}
public function get_parameter($name) {
return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
}
public function get_parameters() {
return $this->parameters;
}
public function unset_parameter($name) {
unset($this->parameters[$name]);
}
/**
* The request parameters, sorted and concatenated into a normalized string.
* @return string
*/
public function get_signable_parameters() {
// Grab all parameters
$params = $this->parameters;
// Remove oauth_signature if present
// Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
if (isset($params['oauth_signature'])) {
unset($params['oauth_signature']);
}
return OAuthUtil::build_http_query($params);
}
/**
* Returns the base string of this request
*
* The base string defined as the method, the url
* and the parameters (normalized), each urlencoded
* and the concated with &.
*/
public function get_signature_base_string() {
$parts = array(
$this->get_normalized_http_method(),
$this->get_normalized_http_url(),
$this->get_signable_parameters()
);
$parts = OAuthUtil::urlencode_rfc3986($parts);
return implode('&', $parts);
}
/**
* just uppercases the http method
*/
public function get_normalized_http_method() {
return strtoupper($this->http_method);
}
/**
* parses the url and rebuilds it to be
* scheme://host/path
*/
public function get_normalized_http_url() {
$parts = parse_url($this->http_url);
$port = @$parts['port'];
$scheme = $parts['scheme'];
$host = $parts['host'];
$path = @$parts['path'];
$port or $port = ($scheme == 'https') ? '443' : '80';
if (($scheme == 'https' && $port != '443')
|| ($scheme == 'http' && $port != '80')) {
$host = "$host:$port";
}
return "$scheme://$host$path";
}
/**
* builds a url usable for a GET request
*/
public function to_url() {
$post_data = $this->to_postdata();
$out = $this->get_normalized_http_url();
if ($post_data) {
$out .= '?'.$post_data;
}
return $out;
}
/**
* builds the data one would send in a POST request
*/
public function to_postdata() {
return OAuthUtil::build_http_query($this->parameters);
}
/**
* builds the Authorization: header
*/
public function to_header() {
$out ='Authorization: OAuth realm=""';
$total = array();
foreach ($this->parameters as $k => $v) {
if (substr($k, 0, 5) != "oauth") continue;
if (is_array($v)) {
throw new OAuthException('Arrays not supported in headers');
}
$out .= ',' .
OAuthUtil::urlencode_rfc3986($k) .
'="' .
OAuthUtil::urlencode_rfc3986($v) .
'"';
}
return $out;
}
public function __toString() {
return $this->to_url();
}
public function sign_request($signature_method, $consumer, $token) {
$this->set_parameter(
"oauth_signature_method",
$signature_method->get_name(),
false
);
$signature = $this->build_signature($signature_method, $consumer, $token);
$this->set_parameter("oauth_signature", $signature, false);
}
public function build_signature($signature_method, $consumer, $token) {
$signature = $signature_method->build_signature($this, $consumer, $token);
return $signature;
}
/**
* util function: current timestamp
*/
private static function generate_timestamp() {
return time();
}
/**
* util function: current nonce
*/
private static function generate_nonce() {
$mt = microtime();
$rand = mt_rand();
return md5($mt . $rand); // md5s look nicer than numbers
}
}
class OAuthServer {
protected $timestamp_threshold = 300; // in seconds, five minutes
protected $version = 1.0; // hi blaine
protected $signature_methods = array();
protected $data_store;
function __construct($data_store) {
$this->data_store = $data_store;
}
public function add_signature_method($signature_method) {
$this->signature_methods[$signature_method->get_name()] =
$signature_method;
}
// high level functions
/**
* process a request_token request
* returns the request token on success
*/
public function fetch_request_token(&$request) {
$this->get_version($request);
$consumer = $this->get_consumer($request);
// no token required for the initial token request
$token = NULL;
$this->check_signature($request, $consumer, $token);
$new_token = $this->data_store->new_request_token($consumer);
return $new_token;
}
/**
* process an access_token request
* returns the access token on success
*/
public function fetch_access_token(&$request) {
$this->get_version($request);
$consumer = $this->get_consumer($request);
// requires authorized request token
$token = $this->get_token($request, $consumer, "request");
$this->check_signature($request, $consumer, $token);
$new_token = $this->data_store->new_access_token($token, $consumer);
return $new_token;
}
/**
* verify an api call, checks all the parameters
*/
public function verify_request(&$request) {
global $OAuth_last_computed_signature;
$OAuth_last_computed_signature = false;
$this->get_version($request);
$consumer = $this->get_consumer($request);
$token = $this->get_token($request, $consumer, "access");
$this->check_signature($request, $consumer, $token);
return array($consumer, $token);
}
// Internals from here
/**
* version 1
*/
private function get_version(&$request) {
$version = $request->get_parameter("oauth_version");
if (!$version) {
$version = 1.0;
}
if ($version && $version != $this->version) {
throw new OAuthException("OAuth version '$version' not supported");
}
return $version;
}
/**
* figure out the signature with some defaults
*/
private function get_signature_method(&$request) {
$signature_method =
@$request->get_parameter("oauth_signature_method");
if (!$signature_method) {
$signature_method = "PLAINTEXT";
}
if (!in_array($signature_method,
array_keys($this->signature_methods))) {
throw new OAuthException(
"Signature method '$signature_method' not supported " .
"try one of the following: " .
implode(", ", array_keys($this->signature_methods))
);
}
return $this->signature_methods[$signature_method];
}
/**
* try to find the consumer for the provided request's consumer key
*/
private function get_consumer(&$request) {
$consumer_key = @$request->get_parameter("oauth_consumer_key");
if (!$consumer_key) {
throw new OAuthException("Invalid consumer key");
}
$consumer = $this->data_store->lookup_consumer($consumer_key);
if (!$consumer) {
throw new OAuthException("Invalid consumer");
}
return $consumer;
}
/**
* try to find the token for the provided request's token key
*/
private function get_token(&$request, $consumer, $token_type="access") {
$token_field = @$request->get_parameter('oauth_token');
if ( !$token_field) return false;
$token = $this->data_store->lookup_token(
$consumer, $token_type, $token_field
);
if (!$token) {
throw new OAuthException("Invalid $token_type token: $token_field");
}
return $token;
}
/**
* all-in-one function to check the signature on a request
* should guess the signature method appropriately
*/
private function check_signature(&$request, $consumer, $token) {
// this should probably be in a different method
global $OAuth_last_computed_signature;
$OAuth_last_computed_signature = false;
$timestamp = @$request->get_parameter('oauth_timestamp');
$nonce = @$request->get_parameter('oauth_nonce');
$this->check_timestamp($timestamp);
$this->check_nonce($consumer, $token, $nonce, $timestamp);
$signature_method = $this->get_signature_method($request);
$signature = $request->get_parameter('oauth_signature');
$valid_sig = $signature_method->check_signature(
$request,
$consumer,
$token,
$signature
);
if (!$valid_sig) {
$ex_text = "Invalid signature";
if ( $OAuth_last_computed_signature ) {
$ex_text = $ex_text . " ours= $OAuth_last_computed_signature yours=$signature";
}
throw new OAuthException($ex_text);
}
}
/**
* check that the timestamp is new enough
*/
private function check_timestamp($timestamp) {
// verify that timestamp is recentish
$now = time();
if ($now - $timestamp > $this->timestamp_threshold) {
throw new OAuthException(
"Expired timestamp, yours $timestamp, ours $now"
);
}
}
/**
* check that the nonce is not repeated
*/
private function check_nonce($consumer, $token, $nonce, $timestamp) {
// verify that the nonce is uniqueish
$found = $this->data_store->lookup_nonce(
$consumer,
$token,
$nonce,
$timestamp
);
if ($found) {
throw new OAuthException("Nonce already used: $nonce");
}
}
}
class OAuthDataStore {
function lookup_consumer($consumer_key) {
// implement me
}
function lookup_token($consumer, $token_type, $token) {
// implement me
}
function lookup_nonce($consumer, $token, $nonce, $timestamp) {
// implement me
}
function new_request_token($consumer) {
// return a new token attached to this consumer
}
function new_access_token($token, $consumer) {
// return a new access token attached to this consumer
// for the user associated with this token if the request token
// is authorized
// should also invalidate the request token
}
}
class OAuthUtil {
public static function urlencode_rfc3986($input) {
if (is_array($input)) {
return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
} else if (is_scalar($input)) {
return str_replace(
'+',
' ',
str_replace('%7E', '~', rawurlencode($input))
);
} else {
return '';
}
}
// This decode function isn't taking into consideration the above
// modifications to the encoding process. However, this method doesn't
// seem to be used anywhere so leaving it as is.
public static function urldecode_rfc3986($string) {
return urldecode($string);
}
// Utility function for turning the Authorization: header into
// parameters, has to do some unescaping
// Can filter out any non-oauth parameters if needed (default behaviour)
public static function split_header($header, $only_allow_oauth_parameters = true) {
$pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
$offset = 0;
$params = array();
while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
$match = $matches[0];
$header_name = $matches[2][0];
$header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) {
$params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content);
}
$offset = $match[1] + strlen($match[0]);
}
if (isset($params['realm'])) {
unset($params['realm']);
}
return $params;
}
// helper to try to sort out headers for people who aren't running apache
public static function get_headers() {
if (function_exists('apache_request_headers')) {
// we need this to get the actual Authorization: header
// because apache tends to tell us it doesn't exist
return apache_request_headers();
}
// otherwise we don't have apache and are just going to have to hope
// that $_SERVER actually contains what we need
$out = array();
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) == "HTTP_") {
// this is chaos, basically it is just there to capitalize the first
// letter of every word that is not an initial HTTP and strip HTTP
// code from przemek
$key = str_replace(
" ",
"-",
ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
);
$out[$key] = $value;
}
}
return $out;
}
// This function takes a input like a=b&a=c&d=e and returns the parsed
// parameters like this
// array('a' => array('b','c'), 'd' => 'e')
public static function parse_parameters( $input ) {
if (!isset($input) || !$input) return array();
$pairs = explode('&', $input);
$parsed_parameters = array();
foreach ($pairs as $pair) {
$split = explode('=', $pair, 2);
$parameter = OAuthUtil::urldecode_rfc3986($split[0]);
$value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : '';
if (isset($parsed_parameters[$parameter])) {
// We have already recieved parameter(s) with this name, so add to the list
// of parameters with this name
if (is_scalar($parsed_parameters[$parameter])) {
// This is the first duplicate, so transform scalar (string) into an array
// so we can add the duplicates
$parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
}
$parsed_parameters[$parameter][] = $value;
} else {
$parsed_parameters[$parameter] = $value;
}
}
return $parsed_parameters;
}
public static function build_http_query($params) {
if (!$params) return '';
// Urlencode both keys and values
$keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
$values = OAuthUtil::urlencode_rfc3986(array_values($params));
$params = array_combine($keys, $values);
// Parameters are sorted by name, using lexicographical byte value ordering.
// Ref: Spec: 9.1.1 (1)
uksort($params, 'strcmp');
$pairs = array();
foreach ($params as $parameter => $value) {
if (is_array($value)) {
// If two or more parameters share the same name, they are sorted by their value
// Ref: Spec: 9.1.1 (1)
natsort($value);
foreach ($value as $duplicate_value) {
$pairs[] = $parameter . '=' . $duplicate_value;
}
} else {
$pairs[] = $parameter . '=' . $value;
}
}
// For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
// Each name-value pair is separated by an '&' character (ASCII code 38)
return implode('&', $pairs);
}
}
?>
+154
View File
@@ -0,0 +1,154 @@
<?php
require_once("OAuth.php");
require_once("TrivialOAuthDataStore.php");
function getLastOAuthBodyBaseString() {
global $LastOAuthBodyBaseString;
return $LastOAuthBodyBaseString;
}
function handleOAuthBodyPOST($oauth_consumer_key, $oauth_consumer_secret)
{
$request_headers = OAuthUtil::get_headers();
// print_r($request_headers);
// Must reject application/x-www-form-urlencoded
if ($request_headers['Content-type'] == 'application/x-www-form-urlencoded' ) {
throw new Exception("OAuth request body signing must not use application/x-www-form-urlencoded");
}
if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
$header_parameters = OAuthUtil::split_header($request_headers['Authorization']);
// echo("HEADER PARMS=\n");
// print_r($header_parameters);
$oauth_body_hash = $header_parameters['oauth_body_hash'];
// echo("OBH=".$oauth_body_hash."\n");
}
if ( ! isset($oauth_body_hash) ) {
throw new Exception("OAuth request body signing requires oauth_body_hash body");
}
// Verify the message signature
$store = new TrivialOAuthDataStore();
$store->add_consumer($oauth_consumer_key, $oauth_consumer_secret);
$server = new OAuthServer($store);
$method = new OAuthSignatureMethod_HMAC_SHA1();
$server->add_signature_method($method);
$request = OAuthRequest::from_request();
global $LastOAuthBodyBaseString;
$LastOAuthBodyBaseString = $request->get_signature_base_string();
// echo($LastOAuthBodyBaseString."\n");
try {
$server->verify_request($request);
} catch (Exception $e) {
$message = $e->getMessage();
throw new Exception("OAuth signature failed: " . $message);
}
$postdata = file_get_contents('php://input');
// echo($postdata);
$hash = base64_encode(sha1($postdata, TRUE));
if ( $hash != $oauth_body_hash ) {
throw new Exception("OAuth oauth_body_hash mismatch");
}
return $postdata;
}
function sendOAuthBodyPOST($method, $endpoint, $oauth_consumer_key, $oauth_consumer_secret, $content_type, $body)
{
global $CFG;
require_once($CFG->dirroot . '/lib/filelib.php');
$hash = base64_encode(sha1($body, TRUE));
$parms = array('oauth_body_hash' => $hash);
$test_token = '';
$hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
$test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL);
$acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms);
$acc_req->sign_request($hmac_method, $test_consumer, $test_token);
// Pass this back up "out of band" for debugging
global $LastOAuthBodyBaseString;
$LastOAuthBodyBaseString = $acc_req->get_signature_base_string();
// echo($LastOAuthBodyBaseString."\m");
$headers = array();
$headers[] = $acc_req->to_header();
$headers[] = "Content-type: " . $content_type;
$curl = new curl();
$curl->setHeader($headers);
$response = $curl->post($endpoint, $body);
return $response;
}
function sendOAuthParamsPOST($method, $endpoint, $oauth_consumer_key, $oauth_consumer_secret, $content_type, $params)
{
if (is_array($params)) {
$body = http_build_query($params, '', '&');
} else {
$body = $params;
}
$hash = base64_encode(sha1($body, TRUE));
$parms = $params;
$parms['oauth_body_hash'] = $hash;
$test_token = '';
$hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
$test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL);
$acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms);
$acc_req->sign_request($hmac_method, $test_consumer, $test_token);
// Pass this back up "out of band" for debugging
global $LastOAuthBodyBaseString;
$LastOAuthBodyBaseString = $acc_req->get_signature_base_string();
// echo($LastOAuthBodyBaseString."\m");
$header = $acc_req->to_header();
$header = $header . "\r\nContent-type: " . $content_type . "\r\n";
$params = array('http' => array(
'method' => 'POST',
'content' => $body,
'header' => $header
));
$ctx = stream_context_create($params);
$fp = @fopen($endpoint, 'rb', false, $ctx);
if (!$fp) {
$message = "(No error message provided.)";
if ($error = error_get_last()) {
$message = $error["message"];
}
throw new \Exception("Problem with $endpoint, $message");
}
$response = @stream_get_contents($fp);
if ($response === false) {
$message = "(No error message provided.)";
if ($error = error_get_last()) {
$message = $error["message"];
}
throw new \Exception("Problem reading data from $endpoint, $message");
}
return $response;
}
?>
@@ -0,0 +1,48 @@
<?php
require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuth.php');
/**
* A Trivial memory-based store - no support for tokens
*/
class TrivialOAuthDataStore extends OAuthDataStore {
private $consumers = array();
function add_consumer($consumer_key, $consumer_secret) {
$this->consumers[$consumer_key] = $consumer_secret;
}
function lookup_consumer($consumer_key) {
if ( strpos($consumer_key, "http://" ) === 0 ) {
$consumer = new OAuthConsumer($consumer_key,"secret", NULL);
return $consumer;
}
if ( $this->consumers[$consumer_key] ) {
$consumer = new OAuthConsumer($consumer_key,$this->consumers[$consumer_key], NULL);
return $consumer;
}
return NULL;
}
function lookup_token($consumer, $token_type, $token) {
return new OAuthToken($consumer, "");
}
// Return NULL if the nonce has not been used
// Return $nonce if the nonce was previously used
function lookup_nonce($consumer, $token, $nonce, $timestamp) {
// Should add some clever logic to keep nonces from
// being reused - for no we are really trusting
// that the timestamp will save us
return NULL;
}
function new_request_token($consumer) {
return NULL;
}
function new_access_token($token, $consumer) {
return NULL;
}
}
?>
+278
View File
@@ -0,0 +1,278 @@
<?php
require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuth.php');
require_once($CFG->dirroot . '/enrol/lti/ims-blti/TrivialOAuthDataStore.php');
// Returns true if this is a Basic LTI message
// with minimum values to meet the protocol
function is_basic_lti_request() {
$good_message_type = $_REQUEST["lti_message_type"] == "basic-lti-launch-request";
$good_lti_version = ($_REQUEST["lti_version"] == "LTI-1p0" or $_REQUEST["lti_version"] == "LTI-1.0");
$good_lti_version = $good_lti_version || ($_REQUEST["lti_version"] == "LTI-2p0" or $_REQUEST["lti_version"] == "LTI-2.0");
$resource_link_id = $_REQUEST["resource_link_id"];
if ($good_message_type and $good_lti_version and isset($resource_link_id) ) return(true);
return false;
}
// Basic LTI Class that does the setup and provides utility
// functions
class BLTI {
public $valid = false;
public $complete = false;
public $message = false;
public $basestring = false;
public $info = false;
public $row = false;
public $context_id = false; // Override context_id
function __construct($parm=false, $usesession=true, $doredirect=true) {
// If this request is not an LTI Launch, either
// give up or try to retrieve the context from session
if ( ! is_basic_lti_request() ) {
if ( $usesession === false ) return;
if ( strlen(session_id()) > 0 ) {
$row = $_SESSION['_basiclti_lti_row'];
if ( isset($row) ) $this->row = $row;
$context_id = $_SESSION['_basiclti_lti_context_id'];
if ( isset($context_id) ) $this->context_id = $context_id;
$info = $_SESSION['_basic_lti_context'];
if ( isset($info) ) {
$this->info = $info;
$this->valid = true;
return;
}
$this->message = "Could not find context in session";
return;
}
$this->message = "Session not available";
return;
}
// Insure we have a valid launch
if ( empty($_REQUEST["oauth_consumer_key"]) ) {
$this->message = "Missing oauth_consumer_key in request";
return;
}
$oauth_consumer_key = $_REQUEST["oauth_consumer_key"];
// Find the secret - either form the parameter as a string or
// look it up in a database from parameters we are given
$secret = false;
$row = false;
if ( is_string($parm) ) {
$secret = $parm;
} else if ( ! is_array($parm) ) {
$this->message = "Constructor requires a secret or database information.";
return;
}
// Verify the message signature
$store = new TrivialOAuthDataStore();
$store->add_consumer($oauth_consumer_key, $secret);
$server = new OAuthServer($store);
$method = new OAuthSignatureMethod_HMAC_SHA1();
$server->add_signature_method($method);
$request = OAuthRequest::from_request();
$this->basestring = $request->get_signature_base_string();
try {
$server->verify_request($request);
$this->valid = true;
} catch (Exception $e) {
$this->message = $e->getMessage();
return;
}
// Store the launch information in the session for later
$newinfo = array();
foreach($_POST as $key => $value ) {
if ( $key == "basiclti_submit" ) continue;
if ( strpos($key, "oauth_") === false ) {
$newinfo[$key] = $value;
continue;
}
if ( $key == "oauth_consumer_key" ) {
$newinfo[$key] = $value;
continue;
}
}
//Added abertranb to decode base 64 20120801
if (isset($newinfo['custom_lti_message_encoded_base64']) && $newinfo['custom_lti_message_encoded_base64']==1){
$newinfo = $this->decodeBase64($newinfo);
}
$this->info = $newinfo;
if ( $usesession == true and strlen(session_id()) > 0 ) {
$_SESSION['_basic_lti_context'] = $this->info;
unset($_SESSION['_basiclti_lti_row']);
unset($_SESSION['_basiclti_lti_context_id']);
if ( $this->row ) $_SESSION['_basiclti_lti_row'] = $this->row;
if ( $this->context_id ) $_SESSION['_basiclti_lti_context_id'] = $this->context_id;
}
if ( $this->valid && $doredirect ) {
$this->redirect();
$this->complete = true;
}
}
function addSession($location) {
if ( ini_get('session.use_cookies') == 0 ) {
if ( strpos($location,'?') > 0 ) {
$location = $location . '&';
} else {
$location = $location . '?';
}
$location = $location . session_name() . '=' . session_id();
}
return $location;
}
function isInstructor() {
$roles = $this->info['roles'];
$roles = strtolower($roles);
if ( ! ( strpos($roles,"instructor") === false ) ) return true;
if ( ! ( strpos($roles,"administrator") === false ) ) return true;
return false;
}
function getUserEmail() {
# set default email in the event privacy settings don't pass in email.
$email = $this->info['user_id'] . "@ltiuser.com";
if ( isset($this->info['lis_person_contact_email_primary']) ) $email = $this->info['lis_person_contact_email_primary'];
# Sakai Hack
if ( isset($this->info['lis_person_contact_emailprimary']) ) $email = $this->info['lis_person_contact_emailprimary'];
return $email;
}
function getUserShortName() {
$email = $this->getUserEmail();
$givenname = $this->info['lis_person_name_given'];
$familyname = $this->info['lis_person_name_family'];
$fullname = $this->info['lis_person_name_full'];
if ( strlen($email) > 0 ) return $email;
if ( strlen($givenname) > 0 ) return $givenname;
if ( strlen($familyname) > 0 ) return $familyname;
return $this->getUserName();
}
function getUserName() {
$givenname = $this->info['lis_person_name_given'];
$familyname = $this->info['lis_person_name_family'];
$fullname = $this->info['lis_person_name_full'];
if ( strlen($fullname) > 0 ) return $fullname;
if ( strlen($familyname) > 0 and strlen($givenname) > 0 ) return $givenname + $familyname;
if ( strlen($givenname) > 0 ) return $givenname;
if ( strlen($familyname) > 0 ) return $familyname;
return $this->getUserEmail();
}
function getUserKey() {
$oauth = $this->info['oauth_consumer_key'];
$id = $this->info['user_id'];
if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id;
return false;
}
function getUserImage() {
$image = $this->info['user_image'];
if ( strlen($image) > 0 ) return $image;
$email = $this->getUserEmail();
if ( $email === false ) return false;
$size = 40;
$grav_url = $_SERVER['HTTPS'] ? 'https://' : 'http://';
$grav_url = $grav_url . "www.gravatar.com/avatar.php?gravatar_id=".md5( strtolower($email) )."&size=".$size;
return $grav_url;
}
function getResourceKey() {
$oauth = $this->info['oauth_consumer_key'];
$id = $this->info['resource_link_id'];
if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id;
return false;
}
function getResourceTitle() {
$title = $this->info['resource_link_title'];
if ( strlen($title) > 0 ) return $title;
return false;
}
function getConsumerKey() {
$oauth = $this->info['oauth_consumer_key'];
return $oauth;
}
function getCourseKey() {
if ( $this->context_id ) return $this->context_id;
$oauth = $this->info['oauth_consumer_key'];
$id = $this->info['context_id'];
if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id;
return false;
}
function getCourseName() {
$label = $this->info['context_label'];
$title = $this->info['context_title'];
$id = $this->info['context_id'];
if ( strlen($label) > 0 ) return $label;
if ( strlen($title) > 0 ) return $title;
if ( strlen($id) > 0 ) return $id;
return false;
}
// TODO: Add javasript version if headers are already sent
function redirect() {
$host = $_SERVER['HTTP_HOST'];
$uri = $_SERVER['PHP_SELF'];
$location = $_SERVER['HTTPS'] ? 'https://' : 'http://';
$location = $location . $host . $uri;
$location = $this->addSession($location);
header("Location: $location");
}
function dump() {
if ( ! $this->valid or $this->info == false ) return "Context not valid\n";
$ret = "";
if ( $this->isInstructor() ) {
$ret .= "isInstructor() = true\n";
} else {
$ret .= "isInstructor() = false\n";
}
$ret .= "getUserKey() = ".$this->getUserKey()."\n";
$ret .= "getUserEmail() = ".$this->getUserEmail()."\n";
$ret .= "getUserShortName() = ".$this->getUserShortName()."\n";
$ret .= "getUserName() = ".$this->getUserName()."\n";
$ret .= "getUserImage() = ".$this->getUserImage()."\n";
$ret .= "getResourceKey() = ".$this->getResourceKey()."\n";
$ret .= "getResourceTitle() = ".$this->getResourceTitle()."\n";
$ret .= "getCourseName() = ".$this->getCourseName()."\n";
$ret .= "getCourseKey() = ".$this->getCourseKey()."\n";
$ret .= "getConsumerKey() = ".$this->getConsumerKey()."\n";
return $ret;
}
/**
* Data submitter are in base64 then we have to decode
* @author Antoni Bertran (antoni@tresipunt.com)
* @param $info array
* @date 20120801
*/
function decodeBase64($info) {
$keysNoEncode = array("lti_version", "lti_message_type", "tool_consumer_instance_description", "tool_consumer_instance_guid", "oauth_consumer_key", "custom_lti_message_encoded_base64", "oauth_nonce", "oauth_version", "oauth_callback", "oauth_timestamp", "basiclti_submit", "oauth_signature_method", "ext_ims_lis_memberships_id", "ext_ims_lis_memberships_url");
foreach ($info as $key => $item){
if (!in_array($key, $keysNoEncode))
$info[$key] = base64_decode($item);
}
return $info;
}
}
?>
+227
View File
@@ -0,0 +1,227 @@
<?php
require_once 'OAuth.php';
// Replace this with some real function that pulls from the LMS.
function getLMSDummyData() {
$parms = array(
"resource_link_id" => "120988f929-274612",
"resource_link_title" => "Weekly Blog",
"resource_link_description" => "Each student needs to reflect on the weekly reading. These should be one paragraph long.",
"user_id" => "292832126",
"roles" => "Instructor", // or Learner
"lis_person_name_full" => 'Jane Q. Public',
"lis_person_contact_email_primary" => "user@school.edu",
"lis_person_sourcedid" => "school.edu:user",
"context_id" => "456434513",
"context_title" => "Design of Personal Environments",
"context_label" => "SI182",
);
return $parms;
}
function validateDescriptor($descriptor)
{
$xml = new SimpleXMLElement($xmldata);
if ( ! $xml ) {
echo("Error parsing Descriptor XML\n");
return;
}
$launch_url = $xml->secure_launch_url[0];
if ( ! $launch_url ) $launch_url = $xml->launch_url[0];
if ( $launch_url ) $launch_url = (string) $launch_url;
return $launch_url;
}
// Parse a descriptor
function launchInfo($xmldata) {
$xml = new SimpleXMLElement($xmldata);
if ( ! $xml ) {
echo("Error parsing Descriptor XML\n");
return;
}
$launch_url = $xml->secure_launch_url[0];
if ( ! $launch_url ) $launch_url = $xml->launch_url[0];
if ( $launch_url ) $launch_url = (string) $launch_url;
$custom = array();
if ( $xml->custom[0]->parameter )
foreach ( $xml->custom[0]->parameter as $resource) {
$key = (string) $resource['key'];
$key = strtolower($key);
$nk = "";
for($i=0; $i < strlen($key); $i++) {
$ch = substr($key,$i,1);
if ( $ch >= "a" && $ch <= "z" ) $nk .= $ch;
else if ( $ch >= "0" && $ch <= "9" ) $nk .= $ch;
else $nk .= "_";
}
$value = (string) $resource;
$custom["custom_".$nk] = $value;
}
return array("launch_url" => $launch_url, "custom" => $custom ) ;
}
function signParameters($oldparms, $endpoint, $method, $oauth_consumer_key, $oauth_consumer_secret,
$submit_text = false, $org_id = false, $org_desc = false)
{
global $last_base_string;
$parms = $oldparms;
if ( ! isset($parms["lti_version"]) ) $parms["lti_version"] = "LTI-1p0";
if ( ! isset($parms["lti_message_type"]) ) $parms["lti_message_type"] = "basic-lti-launch-request";
if ( ! isset($parms["oauth_callback"]) ) $parms["oauth_callback"] = "about:blank";
if ( $org_id ) $parms["tool_consumer_instance_guid"] = $org_id;
if ( $org_desc ) $parms["tool_consumer_instance_description"] = $org_desc;
if ( $submit_text ) $parms["ext_submit"] = $submit_text;
$test_token = '';
$hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
$test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL);
$acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms);
$acc_req->sign_request($hmac_method, $test_consumer, $test_token);
// Pass this back up "out of band" for debugging
$last_base_string = $acc_req->get_signature_base_string();
$newparms = $acc_req->get_parameters();
return $newparms;
}
function signOnly($oldparms, $endpoint, $method, $oauth_consumer_key, $oauth_consumer_secret)
{
global $last_base_string;
$parms = $oldparms;
$test_token = '';
$hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
$test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL);
$acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms);
$acc_req->sign_request($hmac_method, $test_consumer, $test_token);
// Pass this back up "out of band" for debugging
$last_base_string = $acc_req->get_signature_base_string();
$newparms = $acc_req->get_parameters();
return $newparms;
}
function postLaunchHTML($newparms, $endpoint, $debug=false, $iframeattr=false) {
global $last_base_string;
$r = "<div id=\"ltiLaunchFormSubmitArea\">\n";
if ( $iframeattr ) {
$r = "<form action=\"".$endpoint."\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" target=\"basicltiLaunchFrame\" encType=\"application/x-www-form-urlencoded\">\n" ;
} else {
$r = "<form action=\"".$endpoint."\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n" ;
}
$submit_text = $newparms['ext_submit'];
foreach($newparms as $key => $value ) {
$key = htmlspecialchars($key);
$value = htmlspecialchars($value);
if ( $key == "ext_submit" ) {
$r .= "<input type=\"submit\" name=\"";
} else {
$r .= "<input type=\"hidden\" name=\"";
}
$r .= $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_stringIMS("toggle_debug_data","basiclti")."</a>\n";
$r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
$r .= "<b>".get_stringIMS("basiclti_endpoint","basiclti")."</b><br/>\n";
$r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
$r .= "<b>".get_stringIMS("basiclti_parameters","basiclti")."</b><br/>\n";
foreach($newparms as $key => $value ) {
$key = htmlspecialchars($key);
$value = htmlspecialchars($value);
$r .= "$key = $value<br/>\n";
}
$r .= "&nbsp;<br/>\n";
$r .= "<p><b>".get_stringIMS("basiclti_base_string","basiclti")."</b><br/>\n".$last_base_string."</p>\n";
$r .= "</div>\n";
}
$r .= "</form>\n";
if ( $iframeattr ) {
$r .= "<iframe name=\"basicltiLaunchFrame\" id=\"basicltiLaunchFrame\" src=\"\"\n";
$r .= $iframeattr . ">\n<p>".get_stringIMS("frames_required","basiclti")."</p>\n</iframe>\n";
}
if ( ! $debug ) {
$ext_submit = "ext_submit";
$ext_submit_text = $submit_text;
$r .= " <script type=\"text/javascript\"> \n" .
" //<![CDATA[ \n" .
" document.getElementById(\"ltiLaunchForm\").style.display = \"none\";\n" .
" nei = document.createElement('input');\n" .
" nei.setAttribute('type', 'hidden');\n" .
" nei.setAttribute('name', '".$ext_submit."');\n" .
" nei.setAttribute('value', '".$ext_submit_text."');\n" .
" document.getElementById(\"ltiLaunchForm\").appendChild(nei);\n" .
" document.ltiLaunchForm.submit(); \n" .
" //]]> \n" .
" </script> \n";
}
$r .= "</div>\n";
return $r;
}
/* This is a bit of homage to Moodle's pattern of internationalisation */
function get_stringIMS($key,$bundle) {
return $key;
}
function do_post_request($url, $data, $optional_headers = null)
{
$params = array('http' => array(
'method' => 'POST',
'content' => $data
));
if ($optional_headers !== null) {
$header = $optional_headers . "\r\n";
}
// $header = $header . "Content-type: application/x-www-form-urlencoded\r\n";
$params['http']['header'] = $header;
$ctx = stream_context_create($params);
$fp = @fopen($url, 'rb', false, $ctx);
if (!$fp) {
echo @stream_get_contents($fp);
$message = "(No error message provided.)";
if ($error = error_get_last()) {
$message = $error["message"];
}
throw new Exception("Problem with $url, $message");
}
$response = @stream_get_contents($fp);
if ($response === false) {
$message = "(No error message provided.)";
if ($error = error_get_last()) {
$message = $error["message"];
}
throw new Exception("Problem reading data from $url, $message");
}
return $response;
}
+10
View File
@@ -0,0 +1,10 @@
This library was originally published by the IMS at https://code.google.com/p/ims-dev/ which no longer exists. The
current code was taken from https://github.com/jfederico/ims-dev/tree/master/basiclti/php-simple/ims-blti - with
several changes to the code (including bug fixes). As the library is no longer supported upgrades are not possible.
In future releases we should look into using a supported library.
2022-01-05 - MDL-73502 - Removed get_magic_quotes_gpc() use, was returning false since ages ago.
2022-01-20 - MDL-73523 - Conditional openssl_free_key() use, deprecated by PHP 8.0
2022-03-05 - MDL-73520 - replace deprecated php_errormsg with error_get_last(), deprecated by PHP 8.0
2023-05-03 - MDL-77840 - Throw exception on openssl_sign to avoid null reaching base64_encode, deprecated by PHP 8.1
2023-05-17 - MDL-77350 - Added $callback_url property to class OAuthConsumer to comply with PHP 8.2
+164
View File
@@ -0,0 +1,164 @@
<?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/>.
/**
* List the tool provided in a course
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use enrol_lti\local\ltiadvantage\table\published_resources_table;
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot.'/enrol/lti/lib.php');
$courseid = required_param('courseid', PARAM_INT);
$action = optional_param('action', '', PARAM_ALPHA);
$legacy = optional_param('legacy', false, PARAM_BOOL);
if ($action) {
require_sesskey();
$instanceid = required_param('instanceid', PARAM_INT);
$instance = $DB->get_record('enrol', array('id' => $instanceid), '*', MUST_EXIST);
}
$confirm = optional_param('confirm', 0, PARAM_INT);
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$context = context_course::instance($course->id);
require_login($course);
require_capability('moodle/course:enrolreview', $context);
$ltiplugin = enrol_get_plugin('lti');
$canconfig = has_capability('moodle/course:enrolconfig', $context);
$pageurl = new moodle_url('/enrol/lti/index.php', array('courseid' => $courseid, 'legacy' => $legacy));
$PAGE->set_url($pageurl);
$PAGE->set_title(get_string('course') . ': ' . $course->fullname);
$PAGE->set_pagelayout('admin');
// Check if we want to perform any actions.
if ($action) {
if ($action === 'delete') {
if ($ltiplugin->can_delete_instance($instance)) {
if ($confirm) {
$ltiplugin->delete_instance($instance);
redirect($PAGE->url);
}
$yesurl = new moodle_url('/enrol/lti/index.php',
array('courseid' => $course->id,
'action' => 'delete',
'instanceid' => $instance->id,
'confirm' => 1,
'sesskey' => sesskey())
);
$displayname = $ltiplugin->get_instance_name($instance);
$users = $DB->count_records('user_enrolments', array('enrolid' => $instance->id));
if ($users) {
$message = markdown_to_html(get_string('deleteinstanceconfirm', 'enrol',
array('name' => $displayname,
'users' => $users)));
} else {
$message = markdown_to_html(get_string('deleteinstancenousersconfirm', 'enrol',
array('name' => $displayname)));
}
echo $OUTPUT->header();
echo $OUTPUT->confirm($message, $yesurl, $PAGE->url);
echo $OUTPUT->footer();
die();
}
} else if ($action === 'disable') {
if ($ltiplugin->can_hide_show_instance($instance)) {
if ($instance->status != ENROL_INSTANCE_DISABLED) {
$ltiplugin->update_status($instance, ENROL_INSTANCE_DISABLED);
redirect($PAGE->url);
}
}
} else if ($action === 'enable') {
if ($ltiplugin->can_hide_show_instance($instance)) {
if ($instance->status != ENROL_INSTANCE_ENABLED) {
$ltiplugin->update_status($instance, ENROL_INSTANCE_ENABLED);
redirect($PAGE->url);
}
}
}
}
echo $OUTPUT->header();
if ($legacy) {
echo $OUTPUT->heading(get_string('toolsprovided', 'enrol_lti'));
echo html_writer::tag('p', get_string('toolsprovided_help', 'enrol_lti'));
} else {
echo $OUTPUT->heading(get_string('publishedcontent', 'enrol_lti'));
echo html_writer::tag('p', get_string('publishedcontent_help', 'enrol_lti'));
}
echo html_writer::tag('p', $OUTPUT->doc_link('enrol/lti/index', get_string('morehelp')), ['class' => 'helplink']);
// Distinguish between legacy published tools and LTI-Advantage published resources.
$tabs = [
0 => [
new tabobject('0', new moodle_url('/enrol/lti/index.php', ['courseid' => $courseid]),
get_string('lti13', 'enrol_lti')),
new tabobject('1', new moodle_url('/enrol/lti/index.php', ['legacy' => 1, 'courseid' => $courseid]),
get_string('ltilegacy', 'enrol_lti')),
]
];
$selected = $legacy ? '1' : '0';
echo html_writer::div(print_tabs($tabs, $selected, null, null, true), 'lti-resource-publication');
if ($legacy) {
$notify = new \core\output\notification(get_string('ltilegacydeprecatednotice', 'enrol_lti'),
\core\output\notification::NOTIFY_WARNING);
echo $OUTPUT->render($notify);
if (\enrol_lti\helper::count_lti_tools(array('courseid' => $courseid, 'ltiversion' => 'LTI-1p0/LTI-2p0')) > 0) {
$table = new \enrol_lti\manage_table($courseid);
$table->define_baseurl($pageurl);
$table->out(50, false);
} else {
$notify = new \core\output\notification(get_string('notoolsprovided', 'enrol_lti'),
\core\output\notification::NOTIFY_INFO);
echo $OUTPUT->render($notify);
}
} else {
if (\enrol_lti\helper::count_lti_tools(array('courseid' => $courseid, 'ltiversion' => 'LTI-1p3')) > 0) {
$table = new published_resources_table($courseid);
$table->define_baseurl($pageurl);
$table->out(50, false);
} else {
$notify = new \core\output\notification(get_string('nopublishedcontent', 'enrol_lti'),
\core\output\notification::NOTIFY_INFO);
echo $OUTPUT->render($notify);
}
}
if ($ltiplugin->can_add_instance($course->id)) {
echo $OUTPUT->single_button(new moodle_url('/enrol/editinstance.php',
array(
'legacy' => $legacy,
'type' => 'lti',
'courseid' => $course->id,
'returnurl' => new moodle_url('/enrol/lti/index.php', ['courseid' => $course->id, 'legacy' => $legacy]))
),
get_string('add'));
}
echo $OUTPUT->footer();
+37
View File
@@ -0,0 +1,37 @@
<?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 returns an array of available public keys for the LTI 1.3 tool.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use Packback\Lti1p3\JwksEndpoint;
define('NO_DEBUG_DISPLAY', true);
define('NO_MOODLE_COOKIES', true);
require_once(__DIR__ . '/../../config.php');
$privatekey = get_config('enrol_lti', 'lti_13_privatekey');
$key = get_config('enrol_lti', 'lti_13_kid');
$keyendpoint = JwksEndpoint::new([$key => $privatekey]);
@header('Content-Type: application/json; charset=utf-8');
echo json_encode($keyendpoint->getPublicJwks());
+1
View File
@@ -0,0 +1 @@
+199
View File
@@ -0,0 +1,199 @@
<?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/>.
/**
* LTI enrolment plugin version information
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['addcontent'] = 'Add content';
$string['adddeployment:name'] = 'Deployment name';
$string['adddeployment:deploymentid'] = 'Deployment ID';
$string['adddeployment:deploymentid_help'] = 'Each deployment of the tool (this site) in a platform will be assigned a Deployment ID that is unique to the registration. The Deployment ID must be registered with the tool (this site) before launches from the respective tool deployment are allowed.';
$string['adddeployment:invaliddeploymentiderror'] = 'Invalid deployment ID. The deployment ID already exists for this app registration.';
$string['addtocourse'] = 'Add to course';
$string['addtogradebook'] = 'Add to gradebook';
$string['allowframeembedding'] = 'Note: It is recommended that the site administration setting \'Allow frame embedding\' is enabled, so that tools are displayed within a frame rather than in a new window.';
$string['authltimustbeenabled'] = 'Note: This plugin requires the LTI authentication plugin to be enabled too.';
$string['cookiesarerequired'] = 'Cookies are blocked by your browser';
$string['cookiesarerequiredinfo'] = 'This tool can\'t be launched because your browser seems to be blocking third-party cookies.
<br><br>
To use this tool, try changing your browser cookie settings or using a different browser.';
$string['copiedtoclipboard'] = '{$a} copied to clipboard';
$string['copytoclipboard'] = 'Copy to clipboard';
$string['couldnotestablishproxy'] = 'Could not establish proxy with consumer.';
$string['customproperties'] = 'Custom properties';
$string['deeplinkingurl'] = 'Deep linking URL';
$string['deletedactivity'] = 'Deleted activity';
$string['deletedactivityalt'] = 'The activity shared by this instance has been deleted';
$string['deletedactivitydescription'] = 'The activity shared by this instance has been deleted. You can select another activity to share by editing the instance or, if no longer required, you can simply delete the instance. Deleting the instance will remove any associated user enrolments.';
$string['deploymentadd'] = 'Add a deployment';
$string['deploymentaddnotice'] = 'Deployment added';
$string['deploymentdelete'] = 'Delete deployment';
$string['deploymentdeleteconfirm'] = 'Warning: Deleting a deployment will result in a loss of access for any users following resource links tied to this tool deployment in the platform. Member and grade sync services will also be removed for these resources. Are you sure you want to delete deployment ID \'{$a}\'?';
$string['deploymentdeletenotice'] = 'Deployment deleted';
$string['deploymentid'] = 'Deployment ID';
$string['deployments'] = 'Deployments';
$string['deploymentsinfo'] = 'A deployment ID is generated by the platform when the tool is made available in a context. This may occur at the time of registration, or later, when the tool is made available to a specific course or category. The deployment ID must be entered here before launches from the tool deployment are permitted.';
$string['details'] = 'Details';
$string['editplatformdetails'] = 'Edit platform details';
$string['endpointltiversionnotice'] = 'The tool endpoints below are for manual LTI 1.3 setup only. For earlier versions (1.1/2.0), details for consumers can be found on the \'Published as LTI tools\' page, accessible via course navigation.';
$string['enrolenddate'] = 'End date';
$string['enrolenddate_help'] = 'If enabled, users can access until this date only.';
$string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date';
$string['enrolisdisabled'] = 'The \'Publish as LTI tool\' plugin is disabled.';
$string['enrolltiversionincorrect'] = 'The resource is not set up for use over legacy LTI (versions 1.1/2.0). Please contact the administrator of this tool.';
$string['enrolperiod'] = 'Enrolment duration';
$string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves from the remote system. If disabled, the enrolment duration will be unlimited.';
$string['enrolmentfinished'] = 'Enrolment finished.';
$string['enrolmentnotstarted'] = 'Enrolment has not started.';
$string['enrolstartdate'] = 'Start date';
$string['enrolstartdate_help'] = 'If enabled, users can access from this date onward only.';
$string['existingregistrationerror'] = 'A registration already exists for this issuer and client ID.';
$string['frameembeddingnotenabled'] = 'To access the tool, please follow the link below.';
$string['failedrequest'] = 'Failed request. Reason: {$a->reason}';
$string['gradesync'] = 'Grade synchronisation';
$string['gradesync_help'] = 'Whether grades from the tool are sent to the remote system (LTI consumer).';
$string['incorrecttoken'] = 'The token was incorrect. Please check the URL and try again, or contact the administrator of this tool.';
$string['invalidexpiredregistrationurl'] = 'Invalid or expired registration URL. Please check the URL and try again.';
$string['invalidrequest'] = 'Invalid request';
$string['invalidtoolconsumer'] = 'Invalid tool consumer.';
$string['jwksurl'] = 'JWKS URL';
$string['loginurl'] = 'Initiate login URL';
$string['ltiadvdynregerror:invalidopenidconfigjson'] = "There was a problem fetching the OpenID configuration from the platform. The result was not valid JSON. This may also be caused by blocked hosts configuration. Please make sure your site is configured to connect to the platform domain and try again using a new registration URL.";
$string['ltiadvlauncherror:missingid'] = 'Invalid launch data. The custom claim field \'id\' is required to launch an activity or resource over LTI Advantage.';
$string['ltiadvlauncherror:invalidid'] = 'Invalid launch data. The resource \'{$a}\' is unavailable or does not exist.';
$string['ltiadvlauncherror:invalidregistration'] = 'Invalid launch data or tool configuration. A platform registration could not be found (issuer: {$a->platform}, client ID: {$a->clientid}).';
$string['ltiadvlauncherror:invaliddeployment'] = 'Invalid launch data or tool configuration. The deployment could not be found (deployment ID: {$a}).';
$string['ltilegacy'] = 'Legacy LTI (1.1/2.0)';
$string['ltilegacydeprecatednotice'] = 'Legacy LTI (1.1/2.0) tools are deprecated. Content should now be shared using LTI Advantage.';
$string['lti13'] = 'LTI Advantage';
$string['lti13launchdetails'] = 'Launch details';
$string['lti13launchdetails_help'] = 'The launch URL and custom properties are only required when manually configuring a resource link in the platform. Where possible, teachers should favour the content selection (deep linking) process for the creation of resource links.';
$string['ltiversion'] = 'LTI version';
$string['ltiversion_help'] = 'The version of LTI which will be used to access the published content.
LTI Advantage allows content to be published to pre-registered platforms without the need to make changes on the platform whenever new content is published. The security contract is between the platform and the tool.
Legacy versions (1.1 and 2.0), require that a new tool registration is created on the consumer for each piece of newly published content because each piece of published content has its own security contract with the consumer site and must be set up on a case by case basis.';
$string['managedeployments'] = 'Manage deployments';
$string['maxenrolled'] = 'Maximum enrolled users';
$string['maxenrolled_help'] = 'The maximum number of remote users who can access the tool. If set to zero, the number of enrolled users is unlimited.';
$string['maxenrolledreached'] = 'The maximum number of remote users allowed to access the tool has been reached.';
$string['membersync'] = 'User synchronisation';
$string['membersync_help'] = 'Whether a scheduled task synchronises enrolled users in the remote system with enrolments in this course, creating an account for each remote user as necessary, and enrolling or unenrolling them as required.
If set to no, at the moment when a remote user accesses the tool, an account will be created for them and they will be automatically enrolled.';
$string['membersyncmode'] = 'User synchronisation mode';
$string['membersyncmode_help'] = 'Whether remote users should be enrolled and/or unenrolled from this course.';
$string['membersyncmodeenrolandunenrol'] = 'Enrol new and unenrol missing users';
$string['membersyncmodeenrolnew'] = 'Enrol new users';
$string['membersyncmodeunenrolmissing'] = 'Unenrol missing users';
$string['moodle'] = 'Moodle';
$string['nodeployments'] = 'No tool deployments found';
$string['nopublishedcontent'] = 'No resources or activities are published yet';
$string['notoolsprovided'] = 'No tools provided';
$string['noregisteredplatforms'] = 'No registered platforms';
$string['launchdetails'] = 'Launch details';
$string['launchdetails_help'] = 'A cartridge URL (also called configuration URL) plus secret or a launch URL are required for configuring the tool.';
$string['launchurl'] = 'Launch URL';
$string['lti:config'] = 'Configure \'Publish as LTI tool\' instances';
$string['lti:unenrol'] = 'Unenrol users from the course';
$string['opensslconfiginvalid'] = 'LTI 1.3 requires a valid openssl.cnf to be configured and available to your web server. Please contact the site administrator to configure and enable openssl for this site.';
$string['opentool'] = 'Open tool';
$string['publishedcontent'] = 'Published content';
$string['publishedcontent_help'] = 'A published resource or activity can be used in registered platforms via the content selection (deep linking) flow. Additionally, a resource link can be created manually in the platform by using the launch URL and the custom properties provided.';
$string['platformdetails'] = 'Platform details';
$string['platformdetailsinfo'] = 'Once the tool has been set up in the platform, details from the platform must be recorded here to complete the registration.
<ul>
<li>For dynamic registrations, this information will have been set automatically and no further changes are required.</li>
<li>For manual registrations, this information must be manually copied from the platform.</li>
</ul>';
$string['pluginname'] = 'Publish as LTI tool';
$string['pluginname_desc'] = 'The \'Publish as LTI tool\' plugin, together with the LTI authentication plugin, allows remote users to access selected courses and activities. In other words, Moodle functions as an LTI tool provider.';
$string['privacy:metadata:enrol_lti_users'] = 'The list of users enrolled via an LTI provider';
$string['privacy:metadata:enrol_lti_users:userid'] = 'The ID of the user';
$string['privacy:metadata:enrol_lti_users:lastgrade'] = 'The last grade the user was recorded of having';
$string['privacy:metadata:enrol_lti_users:lastaccess'] = 'The time when the user last accessed the course';
$string['privacy:metadata:enrol_lti_users:timecreated'] = 'The time when the user was enrolled';
$string['provisioningmodestudentlaunch'] = 'Student first launch provisioning mode';
$string['provisioningmodeteacherlaunch'] = 'Teacher first launch provisioning mode';
$string['provisioningmode'] = 'Provisioning mode';
$string['provisioningmode_help'] = 'This setting determines how accounts are handled on first launch. Several modes are supported:
<ul>
<li>New accounts only (automatic). Accounts will be automatically created for users launching from the platform. This is the default for student launches.</li>
<li>Existing and new accounts (prompt). The user will be given a choice of what to do. They can decide to link an existing account, or have a new account created for them. This is the most flexible option and the default for teacher launches.</li>
<li>Existing accounts only (prompt). The user will be asked to link an existing account and cannot access the tool resources without doing so.</li>
</ul>';
$string['registerplatform:accesstokenurl'] = 'Access token URL';
$string['registerplatform:accesstokenurl_help'] = 'The URL to which access token requests will be sent by the tool. Will be provided by the platform.';
$string['registerplatform:authrequesturl'] = 'Authentication request URL';
$string['registerplatform:authrequesturl_help'] = 'The URL to which OpenID Connect authentication requests will be sent by the tool. Will be provided by the platform.';
$string['registerplatform:clientid'] = 'Client ID';
$string['registerplatform:clientid_help'] = 'A string used to identify the tool registration on the platform. Will be provided by the platform.';
$string['registerplatform:duplicateregistrationerror'] = 'Invalid client ID. This client ID is already registered for the platform ID provided.';
$string['registerplatform:jwksurl'] = 'Public keyset URL';
$string['registerplatform:jwksurl_help'] = 'The public keyset or JWKS URL, used to get the platform\'s public key. Will be provided by the platform.';
$string['registerplatform:name'] = 'Platform name';
$string['registerplatform:name_help'] = 'A short name describing the platform. This can be changed at any time.';
$string['registerplatform:platformid'] = 'Platform ID (issuer)';
$string['registerplatform:platformid_help'] = 'The URL identifying the third party learning platform. Will be provided by the platform.';
$string['registerplatform:invalidurlerror'] = 'Invalid URL. Have you included http:// or https://?';
$string['registeredplatforms'] = 'Registered platforms';
$string['registeredplatformsltiversionnotice'] = 'The platforms listed below are registered for LTI 1.3 communication. For earlier versions, consumer registration is not required.';
$string['registerplatformadd'] = 'Register a platform';
$string['registerplatformaddnotice'] = 'Platform registration added';
$string['registerplatformdelete'] = 'Delete platform registration';
$string['registerplatformdeleteconfirm'] = 'Are you sure you want to delete the registration for the platform \'{$a}\'? This will also remove all tool deployments stored against this registration.';
$string['registerplatformdeletenotice'] = 'Platform registration deleted';
$string['registerplatformedit'] = 'Edit registration';
$string['registerplatformeditnotice'] = 'Platform registration updated';
$string['registration'] = 'Published tool registration';
$string['registrationdeeplinklabel'] = 'Add content from {$a}';
$string['registrationresourcelinklabel'] = 'Launch content from {$a}';
$string['registrationdynamic'] = 'Dynamic registration';
$string['registrationmanual'] = 'Manual registration';
$string['registrationstatus'] = 'Status';
$string['registrationstatusactive'] = 'Active';
$string['registrationstatuspending'] = 'Pending';
$string['registrationurl'] = 'Registration URL';
$string['registrationurlinfomessage'] = 'If the platform supports dynamic registration, use the registration URL below. For further information on dynamic registration, see the documentation <a href="{$a}">Publish as LTI tool</a>.';
$string['registrationurl_help'] = 'If a registration URL (also called proxy URL) is used, then the tool is automatically configured.';
$string['remotesystem'] = 'Remote system';
$string['requirecompletion'] = 'Require course or activity completion prior to grade synchronisation';
$string['returnurlnotset'] = 'Return URL was not set.';
$string['roleinstructor'] = 'Role for teacher';
$string['roleinstructor_help'] = 'The role assigned in the tool to the remote teacher.';
$string['rolelearner'] = 'Role for student';
$string['rolelearner_help'] = 'The role assigned in the tool to the remote student.';
$string['secret'] = 'Secret';
$string['secret_help'] = 'A string of characters which is shared with the remote system (LTI consumer) to provide access to the tool.';
$string['sharedexternaltools'] = 'Published as LTI tools';
$string['successfulregistration'] = 'Successful registration';
$string['tasksyncgrades'] = 'Publish as LTI tool grade sync';
$string['tasksyncmembers'] = 'Publish as LTI tool users sync';
$string['tooldetails'] = 'Tool details';
$string['toolsprovided'] = 'Published tools';
$string['toolsprovided_help'] = 'A tool may be shared with another site by providing either launch details or a registration URL.';
$string['tooltobeprovided'] = 'Tool to be published';
$string['toolurl'] = 'Tool URL';
$string['userdefaultvalues'] = 'User default values';
$string['viewplatformdetails'] = 'View platform details';
$string['viewtoolendpoints'] = 'View tool endpoints';
$string['cartridgeurl'] = 'Cartridge URL';
+146
View File
@@ -0,0 +1,146 @@
<?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/>.
/**
* Handles LTI 1.3 resource link launches.
*
* See enrol/lti/launch_deeplink.php for deep linking launches.
*
* There are 2 pathways through this page:
* 1. When first making a resource linking launch from the platform. The launch data is cached at this point, pending user
* authentication, and the page is set such that the post-authentication redirect will return here.
* 2. The post-authentication redirect. The launch data is fetched from the session launch cache, and the resource is displayed.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core\http_client;
use enrol_lti\local\ltiadvantage\lib\lti_cookie;
use enrol_lti\local\ltiadvantage\lib\issuer_database;
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\context_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
use enrol_lti\local\ltiadvantage\service\tool_launch_service;
use enrol_lti\local\ltiadvantage\utility\message_helper;
use Packback\Lti1p3\LtiMessageLaunch;
use Packback\Lti1p3\LtiServiceConnector;
require_once(__DIR__ . '/../../config.php');
global $CFG;
require_once($CFG->libdir . '/filelib.php');
$idtoken = optional_param('id_token', null, PARAM_RAW);
$launchid = optional_param('launchid', null, PARAM_RAW);
if (!is_enabled_auth('lti')) {
throw new moodle_exception('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti'));
}
if (!enrol_is_enabled('lti')) {
throw new moodle_exception('enrolisdisabled', 'enrol_lti');
}
if (empty($idtoken) && empty($launchid)) {
throw new coding_exception('Error: launch requires id_token');
}
// Support caching the launch and retrieving it after the account binding process described in auth::complete_login().
$sesscache = new launch_cache_session();
$issdb = new issuer_database(new application_registration_repository(), new deployment_repository());
$cookie = new lti_cookie();
$serviceconnector = new LtiServiceConnector($sesscache, new http_client());
if ($idtoken) {
$messagelaunch = LtiMessageLaunch::new($issdb, $sesscache, $cookie, $serviceconnector)
->initialize($_POST);
}
if ($launchid) {
$messagelaunch = LtiMessageLaunch::fromCache($launchid, $issdb, $sesscache, $cookie, $serviceconnector);
}
if (empty($messagelaunch)) {
throw new moodle_exception('Bad launch. Message launch data could not be found');
}
// Authenticate the platform user, which could be an instructor, an admin or a learner.
// Auth code needs to be told about consumer secrets for the purposes of migration, since these reside in enrol_lti.
$launchdata = $messagelaunch->getLaunchData();
if (!empty($launchdata['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key'])) {
$legacyconsumerrepo = new legacy_consumer_repository();
$legacyconsumersecrets = $legacyconsumerrepo->get_consumer_secrets(
$launchdata['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key']
);
}
// To authenticate, we need the resource's account provisioning mode for the given LTI role.
if (empty($launchdata['https://purl.imsglobal.org/spec/lti/claim/custom']['id'])) {
throw new \moodle_exception('ltiadvlauncherror:missingid', 'enrol_lti');
}
$resourceuuid = $launchdata['https://purl.imsglobal.org/spec/lti/claim/custom']['id'];
$resource = array_values(\enrol_lti\helper::get_lti_tools(['uuid' => $resourceuuid]));
$resource = $resource[0] ?? null;
if (empty($resource) || $resource->status != ENROL_INSTANCE_ENABLED) {
throw new \moodle_exception('ltiadvlauncherror:invalidid', 'enrol_lti', '', $resourceuuid);
}
$provisioningmode = message_helper::is_instructor_launch($launchdata) ? $resource->provisioningmodeinstructor
: $resource->provisioningmodelearner;
$auth = get_auth_plugin('lti');
$auth->complete_login(
$messagelaunch->getLaunchData(),
new moodle_url('/enrol/lti/launch.php', ['launchid' => $messagelaunch->getLaunchId()]),
$provisioningmode,
$legacyconsumersecrets ?? []
);
global $USER, $CFG, $PAGE;
// Page URL must be set before the require_login check, so that things like policies can redirect back with the launchid.
$PAGE->set_url(new moodle_url('/enrol/lti/launch.php'), ['launchid' => $messagelaunch->getLaunchId()]);
require_login(null, false);
$PAGE->set_context(context_system::instance());
$PAGE->set_pagelayout('popup'); // Same layout as the tool.php page in Legacy 1.1/2.0 launches.
$PAGE->set_title(get_string('opentool', 'enrol_lti'));
$toollaunchservice = new tool_launch_service(
new deployment_repository(),
new application_registration_repository(),
new resource_link_repository(),
new user_repository(),
new context_repository()
);
[$userid, $resource] = $toollaunchservice->user_launches_tool($USER, $messagelaunch);
$context = context::instance_by_id($resource->contextid);
if ($context->contextlevel == CONTEXT_COURSE) {
$courseid = $context->instanceid;
$redirecturl = new moodle_url('/course/view.php', ['id' => $courseid]);
} else if ($context->contextlevel == CONTEXT_MODULE) {
$cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
$redirecturl = new moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]);
} else {
throw new moodle_exception('invalidcontext');
}
if (empty($CFG->allowframembedding)) {
$stropentool = get_string('opentool', 'enrol_lti');
echo html_writer::tag('p', get_string('frameembeddingnotenabled', 'enrol_lti'));
echo html_writer::link($redirecturl, $stropentool, ['target' => '_blank']);
} else {
redirect($redirecturl);
}
+99
View File
@@ -0,0 +1,99 @@
<?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/>.
/**
* Handles LTI 1.3 deep linking launches.
*
* There are 2 pathways through this page:
* 1. When first making a deep linking launch from the platform. The launch data is cached at this point, pending user
* authentication, and the page is set such that the post-authentication redirect will return here.
* 2. The post-authentication redirect. The launch data is fetched from the session launch cache, and the resource
* selection view is rendered.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core\http_client;
use enrol_lti\local\ltiadvantage\lib\lti_cookie;
use enrol_lti\local\ltiadvantage\lib\issuer_database;
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\published_resource_repository;
use Packback\Lti1p3\LtiMessageLaunch;
use Packback\Lti1p3\LtiServiceConnector;
require_once(__DIR__ . '/../../config.php');
global $OUTPUT, $PAGE, $CFG;
require_once($CFG->libdir . '/filelib.php');
$idtoken = optional_param('id_token', null, PARAM_RAW);
$launchid = optional_param('launchid', null, PARAM_RAW);
if (!is_enabled_auth('lti')) {
throw new moodle_exception('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti'));
}
if (!enrol_is_enabled('lti')) {
throw new moodle_exception('enrolisdisabled', 'enrol_lti');
}
if (empty($idtoken) && empty($launchid)) {
throw new coding_exception('Error: launch requires id_token');
}
// First launch from the platform: get launch data and cache it in case the user's not authenticated.
$sesscache = new launch_cache_session();
$issdb = new issuer_database(new application_registration_repository(), new deployment_repository());
$cookie = new lti_cookie();
$serviceconnector = new LtiServiceConnector($sesscache, new http_client());
if ($idtoken) {
$messagelaunch = LtiMessageLaunch::new($issdb, $sesscache, $cookie, $serviceconnector)
->initialize($_POST);
}
if ($launchid) {
$messagelaunch = LtiMessageLaunch::fromCache($launchid, $issdb, $sesscache, $cookie, $serviceconnector);
}
if (empty($messagelaunch)) {
throw new moodle_exception('Bad launch. Deep linking launch data could not be found');
}
// Authenticate the instructor.
// Deep linking cannot use resource-specific provisioning modes, so it just uses a sensible 'existing accounts only' mode.
$auth = get_auth_plugin('lti');
$auth->complete_login(
$messagelaunch->getLaunchData(),
new moodle_url('/enrol/lti/launch_deeplink.php', ['launchid' => $messagelaunch->getLaunchId()]),
auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY
);
require_login(null, false);
global $USER, $CFG;
$PAGE->set_context(context_system::instance());
$url = new moodle_url('/enrol/lti/launch_deeplink.php');
$PAGE->set_url($url);
$PAGE->set_pagelayout('popup');
$PAGE->set_title(get_string('opentool', 'enrol_lti'));
// Get all the published_resource view objects and render them for selection.
global $USER;
$resourcerepo = new published_resource_repository();
$resources = $resourcerepo->find_all_for_user($USER->id);
$renderer = $PAGE->get_renderer('enrol_lti');
echo $OUTPUT->header();
echo $renderer->render_published_resource_selection_view($messagelaunch, $resources);
echo $OUTPUT->footer();
+496
View File
@@ -0,0 +1,496 @@
<?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/>.
/**
* LTI enrolment plugin main library file.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use enrol_lti\data_connector;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use IMSGlobal\LTI\ToolProvider\ToolConsumer;
defined('MOODLE_INTERNAL') || die();
/**
* LTI enrolment plugin class.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class enrol_lti_plugin extends enrol_plugin {
/**
* Return true if we can add a new instance to this course.
*
* @param int $courseid
* @return boolean
*/
public function can_add_instance($courseid) {
$context = context_course::instance($courseid, MUST_EXIST);
return has_capability('moodle/course:enrolconfig', $context) && has_capability('enrol/lti:config', $context);
}
/**
* Is it possible to delete enrol instance via standard UI?
*
* @param object $instance
* @return bool
*/
public function can_delete_instance($instance) {
$context = context_course::instance($instance->courseid);
return has_capability('enrol/lti:config', $context);
}
/**
* Is it possible to hide/show enrol instance via standard UI?
*
* @param stdClass $instance
* @return bool
*/
public function can_hide_show_instance($instance) {
$context = context_course::instance($instance->courseid);
return has_capability('enrol/lti:config', $context);
}
/**
* Returns true if it's possible to unenrol users.
*
* @param stdClass $instance course enrol instance
* @return bool
*/
public function allow_unenrol(stdClass $instance) {
return true;
}
/**
* We are a good plugin and don't invent our own UI/validation code path.
*
* @return boolean
*/
public function use_standard_editing_ui() {
return true;
}
/**
* Add new instance of enrol plugin.
*
* @param object $course
* @param array $fields instance fields
* @return int id of new instance, null if can not be created
*/
public function add_instance($course, array $fields = null) {
global $DB;
$instanceid = parent::add_instance($course, $fields);
// Add additional data to our table.
$data = new stdClass();
$data->enrolid = $instanceid;
$data->timecreated = time();
$data->timemodified = $data->timecreated;
foreach ($fields as $field => $value) {
$data->$field = $value;
}
// LTI Advantage: make a unique identifier for the published resource.
if (empty($data->ltiversion) || $data->ltiversion == 'LTI-1p3') {
$data->uuid = \core\uuid::generate();
}
$DB->insert_record('enrol_lti_tools', $data);
return $instanceid;
}
/**
* Update instance of enrol plugin.
*
* @param stdClass $instance
* @param stdClass $data modified instance fields
* @return boolean
*/
public function update_instance($instance, $data) {
global $DB;
parent::update_instance($instance, $data);
// Remove the fields we don't want to override.
unset($data->id);
unset($data->timecreated);
unset($data->timemodified);
// Convert to an array we can loop over.
$fields = (array) $data;
// Update the data in our table.
$tool = new stdClass();
$tool->id = $data->toolid;
$tool->timemodified = time();
foreach ($fields as $field => $value) {
$tool->$field = $value;
}
// LTI Advantage: make a unique identifier for the published resource.
if ($tool->ltiversion == 'LTI-1p3' && empty($tool->uuid)) {
$tool->uuid = \core\uuid::generate();
}
return $DB->update_record('enrol_lti_tools', $tool);
}
/**
* Delete plugin specific information.
*
* @param stdClass $instance
* @return void
*/
public function delete_instance($instance) {
global $DB;
// Get the tool associated with this instance.
$tool = $DB->get_record('enrol_lti_tools', array('enrolid' => $instance->id), 'id', MUST_EXIST);
// LTI Advantage: delete any resource_link and user_resource_link mappings.
$resourcelinkrepo = new resource_link_repository();
$resourcelinkrepo->delete_by_resource($tool->id);
// Delete any users associated with this tool.
$DB->delete_records('enrol_lti_users', array('toolid' => $tool->id));
// Get tool and consumer mappings.
$rsmapping = $DB->get_recordset('enrol_lti_tool_consumer_map', array('toolid' => $tool->id));
// Delete consumers that are linked to this tool and their related data.
$dataconnector = new data_connector();
foreach ($rsmapping as $mapping) {
$consumer = new ToolConsumer(null, $dataconnector);
$consumer->setRecordId($mapping->consumerid);
$dataconnector->deleteToolConsumer($consumer);
}
$rsmapping->close();
// Delete mapping records.
$DB->delete_records('enrol_lti_tool_consumer_map', array('toolid' => $tool->id));
// Delete the lti tool record.
$DB->delete_records('enrol_lti_tools', array('id' => $tool->id));
// Time for the parent to do it's thang, yeow.
parent::delete_instance($instance);
}
/**
* Handles un-enrolling a user.
*
* @param stdClass $instance
* @param int $userid
* @return void
*/
public function unenrol_user(stdClass $instance, $userid) {
global $DB;
// Get the tool associated with this instance. Note - it may not exist if we have deleted
// the tool. This is fine because we have already cleaned the 'enrol_lti_users' table.
if ($tool = $DB->get_record('enrol_lti_tools', array('enrolid' => $instance->id), 'id')) {
// Need to remove the user from the users table.
$DB->delete_records('enrol_lti_users', array('userid' => $userid, 'toolid' => $tool->id));
}
parent::unenrol_user($instance, $userid);
}
/**
* Add elements to the edit instance form.
*
* @param stdClass $instance
* @param MoodleQuickForm $mform
* @param context $context
* @return bool
*/
public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
global $DB;
$versionoptions = [
'LTI-1p3' => get_string('lti13', 'enrol_lti'),
'LTI-1p0/LTI-2p0' => get_string('ltilegacy', 'enrol_lti')
];
$mform->addElement('select', 'ltiversion', get_string('ltiversion', 'enrol_lti'), $versionoptions);
$mform->addHelpButton('ltiversion', 'ltiversion', 'enrol_lti');
$legacy = optional_param('legacy', 0, PARAM_INT);
if (empty($instance->id)) {
$mform->setDefault('ltiversion', $legacy ? 'LTI-1p0/LTI-2p0' : 'LTI-1p3');
}
$nameattribs = array('size' => '20', 'maxlength' => '255');
$mform->addElement('text', 'name', get_string('custominstancename', 'enrol'), $nameattribs);
$mform->setType('name', PARAM_TEXT);
$mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'server');
$tools = array();
$tools[$context->id] = get_string('course');
$modinfo = get_fast_modinfo($instance->courseid);
$mods = $modinfo->get_cms();
foreach ($mods as $mod) {
$tools[$mod->context->id] = format_string($mod->name);
}
$mform->addElement('select', 'contextid', get_string('tooltobeprovided', 'enrol_lti'), $tools);
$mform->setDefault('contextid', $context->id);
$mform->addElement('duration', 'enrolperiod', get_string('enrolperiod', 'enrol_lti'),
array('optional' => true, 'defaultunit' => DAYSECS));
$mform->setDefault('enrolperiod', 0);
$mform->addHelpButton('enrolperiod', 'enrolperiod', 'enrol_lti');
$mform->addElement('date_time_selector', 'enrolstartdate', get_string('enrolstartdate', 'enrol_lti'),
array('optional' => true));
$mform->setDefault('enrolstartdate', 0);
$mform->addHelpButton('enrolstartdate', 'enrolstartdate', 'enrol_lti');
$mform->addElement('date_time_selector', 'enrolenddate', get_string('enrolenddate', 'enrol_lti'),
array('optional' => true));
$mform->setDefault('enrolenddate', 0);
$mform->addHelpButton('enrolenddate', 'enrolenddate', 'enrol_lti');
$mform->addElement('text', 'maxenrolled', get_string('maxenrolled', 'enrol_lti'));
$mform->setDefault('maxenrolled', 0);
$mform->addHelpButton('maxenrolled', 'maxenrolled', 'enrol_lti');
$mform->setType('maxenrolled', PARAM_INT);
$assignableroles = get_assignable_roles($context);
$mform->addElement('select', 'roleinstructor', get_string('roleinstructor', 'enrol_lti'), $assignableroles);
$mform->setDefault('roleinstructor', '3');
$mform->addHelpButton('roleinstructor', 'roleinstructor', 'enrol_lti');
$mform->addElement('select', 'rolelearner', get_string('rolelearner', 'enrol_lti'), $assignableroles);
$mform->setDefault('rolelearner', '5');
$mform->addHelpButton('rolelearner', 'rolelearner', 'enrol_lti');
if (!$legacy) {
global $CFG;
require_once($CFG->dirroot . '/auth/lti/auth.php');
$authmodes = [
auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY => get_string('provisioningmodeauto', 'auth_lti'),
auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING => get_string('provisioningmodenewexisting', 'auth_lti'),
auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY => get_string('provisioningmodeexistingonly', 'auth_lti')
];
$mform->addElement('select', 'provisioningmodeinstructor', get_string('provisioningmodeteacherlaunch', 'enrol_lti'),
$authmodes);
$mform->addHelpButton('provisioningmodeinstructor', 'provisioningmode', 'enrol_lti');
$mform->setDefault('provisioningmodeinstructor', auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING);
$mform->addElement('select', 'provisioningmodelearner', get_string('provisioningmodestudentlaunch', 'enrol_lti'),
$authmodes);
$mform->addHelpButton('provisioningmodelearner', 'provisioningmode', 'enrol_lti');
$mform->setDefault('provisioningmodelearner', auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY);
}
$mform->addElement('header', 'remotesystem', get_string('remotesystem', 'enrol_lti'));
$mform->addElement('text', 'secret', get_string('secret', 'enrol_lti'), 'maxlength="64" size="25"');
$mform->setType('secret', PARAM_ALPHANUM);
$mform->setDefault('secret', random_string(32));
$mform->addHelpButton('secret', 'secret', 'enrol_lti');
$mform->hideIf('secret', 'ltiversion', 'eq', 'LTI-1p3');
$mform->addElement('selectyesno', 'gradesync', get_string('gradesync', 'enrol_lti'));
$mform->setDefault('gradesync', 1);
$mform->addHelpButton('gradesync', 'gradesync', 'enrol_lti');
$mform->addElement('selectyesno', 'gradesynccompletion', get_string('requirecompletion', 'enrol_lti'));
$mform->setDefault('gradesynccompletion', 0);
$mform->disabledIf('gradesynccompletion', 'gradesync', 0);
$mform->addElement('selectyesno', 'membersync', get_string('membersync', 'enrol_lti'));
$mform->setDefault('membersync', 1);
$mform->addHelpButton('membersync', 'membersync', 'enrol_lti');
$options = array();
$options[\enrol_lti\helper::MEMBER_SYNC_ENROL_AND_UNENROL] = get_string('membersyncmodeenrolandunenrol', 'enrol_lti');
$options[\enrol_lti\helper::MEMBER_SYNC_ENROL_NEW] = get_string('membersyncmodeenrolnew', 'enrol_lti');
$options[\enrol_lti\helper::MEMBER_SYNC_UNENROL_MISSING] = get_string('membersyncmodeunenrolmissing', 'enrol_lti');
$mform->addElement('select', 'membersyncmode', get_string('membersyncmode', 'enrol_lti'), $options);
$mform->setDefault('membersyncmode', \enrol_lti\helper::MEMBER_SYNC_ENROL_AND_UNENROL);
$mform->addHelpButton('membersyncmode', 'membersyncmode', 'enrol_lti');
$mform->disabledIf('membersyncmode', 'membersync', 0);
$mform->addElement('header', 'defaultheader', get_string('userdefaultvalues', 'enrol_lti'));
$emaildisplay = get_config('enrol_lti', 'emaildisplay');
$choices = array(
0 => get_string('emaildisplayno'),
1 => get_string('emaildisplayyes'),
2 => get_string('emaildisplaycourse')
);
$mform->addElement('select', 'maildisplay', get_string('emaildisplay'), $choices);
$mform->setDefault('maildisplay', $emaildisplay);
$mform->addHelpButton('maildisplay', 'emaildisplay');
$city = get_config('enrol_lti', 'city');
$mform->addElement('text', 'city', get_string('city'), 'maxlength="100" size="25"');
$mform->setType('city', PARAM_TEXT);
$mform->setDefault('city', $city);
$country = get_config('enrol_lti', 'country');
$countries = array('' => get_string('selectacountry') . '...') + get_string_manager()->get_list_of_countries();
$mform->addElement('select', 'country', get_string('selectacountry'), $countries);
$mform->setDefault('country', $country);
$mform->setAdvanced('country');
$timezone = get_config('enrol_lti', 'timezone');
$choices = core_date::get_list_of_timezones(null, true);
$mform->addElement('select', 'timezone', get_string('timezone'), $choices);
$mform->setDefault('timezone', $timezone);
$mform->setAdvanced('timezone');
$lang = get_config('enrol_lti', 'lang');
$mform->addElement('select', 'lang', get_string('preferredlanguage'), get_string_manager()->get_list_of_translations());
$mform->setDefault('lang', $lang);
$mform->setAdvanced('lang');
$institution = get_config('enrol_lti', 'institution');
$mform->addElement('text', 'institution', get_string('institution'), 'maxlength="40" size="25"');
$mform->setType('institution', core_user::get_property_type('institution'));
$mform->setDefault('institution', $institution);
$mform->setAdvanced('institution');
// Check if we are editing an instance.
if (!empty($instance->id)) {
// Get the details from the enrol_lti_tools table.
$ltitool = $DB->get_record('enrol_lti_tools', array('enrolid' => $instance->id), '*', MUST_EXIST);
$mform->addElement('hidden', 'toolid');
$mform->setType('toolid', PARAM_INT);
$mform->setConstant('toolid', $ltitool->id);
$mform->addElement('hidden', 'uuid');
$mform->setType('uuid', PARAM_ALPHANUMEXT);
$mform->setConstant('uuid', $ltitool->uuid);
$mform->setDefaults((array) $ltitool);
}
}
/**
* Perform custom validation of the data used to edit the instance.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @param object $instance The instance loaded from the DB
* @param context $context The context of the instance we are editing
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK.
* @return void
*/
public function edit_instance_validation($data, $files, $instance, $context) {
global $COURSE, $DB;
$errors = array();
// Secret must be set.
if (empty($data['secret'])) {
$errors['secret'] = get_string('required');
}
if (!empty($data['enrolenddate']) && $data['enrolenddate'] < $data['enrolstartdate']) {
$errors['enrolenddate'] = get_string('enrolenddateerror', 'enrol_lti');
}
if (!empty($data['requirecompletion'])) {
$completion = new completion_info($COURSE);
$moodlecontext = $DB->get_record('context', array('id' => $data['contextid']));
if ($moodlecontext->contextlevel == CONTEXT_MODULE) {
$cm = get_coursemodule_from_id(false, $moodlecontext->instanceid, 0, false, MUST_EXIST);
} else {
$cm = null;
}
if (!$completion->is_enabled($cm)) {
$errors['requirecompletion'] = get_string('errorcompletionenabled', 'enrol_lti');
}
}
return $errors;
}
/**
* Restore instance and map settings.
*
* @param restore_enrolments_structure_step $step
* @param stdClass $data
* @param stdClass $course
* @param int $oldid
*/
public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
// We want to call the parent because we do not want to add an enrol_lti_tools row
// as that is done as part of the restore process.
$instanceid = parent::add_instance($course, (array)$data);
$step->set_mapping('enrol', $oldid, $instanceid);
}
}
/**
* Display the LTI link in the course administration menu.
*
* @param settings_navigation $navigation The settings navigation object
* @param stdClass $course The course
* @param stdclass $context Course context
*/
function enrol_lti_extend_navigation_course($navigation, $course, $context) {
// Check that the LTI plugin is enabled.
if (enrol_is_enabled('lti')) {
// Check that they can add an instance.
$ltiplugin = enrol_get_plugin('lti');
if ($ltiplugin->can_add_instance($course->id)) {
$url = new moodle_url('/enrol/lti/index.php', ['courseid' => $course->id]);
$settingsnode = navigation_node::create(get_string('sharedexternaltools', 'enrol_lti'), $url,
navigation_node::TYPE_SETTING, null, 'publishedtools', new pix_icon('i/settings', ''));
$navigation->add_node($settingsnode);
}
}
}
/**
* Get icon mapping for font-awesome.
*/
function enrol_lti_get_fontawesome_icon_map() {
return [
'enrol_lti:managedeployments' => 'fa-sitemap',
'enrol_lti:platformdetails' => 'fa-pencil-square-o',
'enrol_lti:enrolinstancewarning' => 'fa-exclamation-circle text-danger',
];
}
/**
* Pre-delete course module hook which disables any methods referring to the deleted module, preventing launches and allowing remap.
*
* @param stdClass $cm The deleted course module record.
*/
function enrol_lti_pre_course_module_delete(stdClass $cm) {
global $DB;
$sql = "id IN (SELECT t.enrolid
FROM {enrol_lti_tools} t
JOIN {context} c ON (t.contextid = c.id)
WHERE c.contextlevel = :contextlevel
AND c.instanceid = :cmid)";
$DB->set_field_select('enrol', 'status', ENROL_INSTANCE_DISABLED, $sql, ['contextlevel' => CONTEXT_MODULE, 'cmid' => $cm->id]);
}
+111
View File
@@ -0,0 +1,111 @@
<?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/>.
/**
* LTI 1.3 login endpoint.
*
* See: http://www.imsglobal.org/spec/security/v1p0/#step-1-third-party-initiated-login
*
* This must support both POST and GET methods, as per the spec.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use auth_lti\local\ltiadvantage\utility\cookie_helper;
use enrol_lti\local\ltiadvantage\lib\lti_cookie;
use enrol_lti\local\ltiadvantage\lib\issuer_database;
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use Packback\Lti1p3\LtiOidcLogin;
require_once(__DIR__."/../../config.php");
// Required fields for OIDC 3rd party initiated login.
// See http://www.imsglobal.org/spec/security/v1p0/#step-1-third-party-initiated-login.
// Validate these here, despite further validation in the LTI 1.3 library.
$iss = required_param('iss', PARAM_URL); // Issuer URI of the calling platform.
$loginhint = required_param('login_hint', PARAM_RAW); // Platform ID for the person to login.
$targetlinkuri = required_param('target_link_uri', PARAM_URL); // The took launch URL.
// Optional lti_message_hint. See https://www.imsglobal.org/spec/lti/v1p3#additional-login-parameters-0.
// If found, this must be returned unmodified to the platform.
$ltimessagehint = optional_param('lti_message_hint', null, PARAM_RAW);
// The target_link_uri param should contain the endpoint that will be executed at the end of the OIDC login process.
// In Moodle, this will either be:
// - enrol/lti/launch.php endpoint (for regular resource link launches) or
// - enrol/lti/launch_deeplink.php endpoint (for deep linking launches)
// Thus, the target_link_uri signifies intent to perform a certain launch type. It can be used to generate the
// redirect_uri param for the auth request but must first be verified, as it is unsigned data at this stage.
// See here: https://www.imsglobal.org/spec/lti/v1p3/impl#verify-the-target_link_uri.
//
// Also note that final redirection to the resource (after the login process is complete) should rely on the
// https://purl.imsglobal.org/spec/lti/claim/target_link_uri claim instead of the target_link_uri value provided here.
// See here: http://www.imsglobal.org/spec/lti/v1p3/#target-link-uri.
$validuris = [
(new moodle_url('/enrol/lti/launch.php'))->out(false), // Resource link launches.
(new moodle_url('/enrol/lti/launch_deeplink.php'))->out(false) // Deep linking launches.
];
// This code verifies the target_link_uri. Only two values are permitted (see endpoints listed above).
if (!in_array($targetlinkuri, $validuris)) {
$msg = 'The target_link_uri param must match one of the redirect URIs set during tool registration.';
throw new coding_exception($msg);
}
// Because client_id is optional, this endpoint receives a param 'id', a unique id generated when creating the registration.
// A registration can thus be located by either the tuple {iss, client_id} (if client_id is provided), or by the tuple {iss, id},
// (if client_id is not provided). See https://www.imsglobal.org/spec/lti/v1p3/#client_id-login-parameter.
global $_REQUEST;
if (empty($_REQUEST['client_id']) && !empty($_REQUEST['id'])) {
$_REQUEST['client_id'] = $_REQUEST['id'];
}
// Before beginning the OIDC authentication, ensure the MoodleSession cookie can be used. Browser-specific steps may need to be
// taken to set cookies in 3rd party contexts. Skip the check if the user is already auth'd. This means that either cookies aren't
// an issue in the current browser/launch context.
if (!isloggedin()) {
cookie_helper::do_cookie_check(new moodle_url('/enrol/lti/login.php', [
'iss' => $iss,
'login_hint' => $loginhint,
'target_link_uri' => $targetlinkuri,
'lti_message_hint' => $ltimessagehint,
'client_id' => $_REQUEST['client_id'],
]));
if (!cookie_helper::cookies_supported()) {
global $OUTPUT, $PAGE;
$PAGE->set_context(context_system::instance());
$PAGE->set_url(new moodle_url('/enrol/lti/login.php'));
$PAGE->set_pagelayout('popup');
echo $OUTPUT->header();
$renderer = $PAGE->get_renderer('enrol_lti');
echo $renderer->render_cookies_required_notice();
echo $OUTPUT->footer();
die();
}
}
// Now, do the OIDC login.
$redirecturl = LtiOidcLogin::new(
new issuer_database(new application_registration_repository(), new deployment_repository()),
new launch_cache_session(),
new lti_cookie()
)->getRedirectUrl($targetlinkuri, $_REQUEST);
redirect($redirecturl);
+131
View File
@@ -0,0 +1,131 @@
<?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/>.
/**
* LTI 1.3 page to create or delete deployments.
*
* This page is only used by LTI 1.3. Older versions do not require platforms to be registered with the tool during
* registration.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core\output\notification;
use enrol_lti\local\ltiadvantage\form\deployment_form;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\context_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
use enrol_lti\local\ltiadvantage\service\tool_deployment_service;
require_once(__DIR__ . '/../../config.php');
global $CFG, $OUTPUT;
require_once($CFG->libdir . '/adminlib.php');
require_once($CFG->dirroot . '/enrol/lti/lib.php');
$action = required_param('action', PARAM_ALPHA);
$registrationid = required_param('registrationid', PARAM_INT);
if (!in_array($action, ['add', 'delete'])) {
throw new coding_exception("Invalid action param '$action'");
}
// The page to go back to when the respective action has been performed.
$deploymentslisturl = new moodle_url($CFG->wwwroot . "/enrol/lti/register_platform.php",
['regid' => $registrationid, 'action' => 'view', 'tabselect' => 'tooldeployments']);
// Local anon helper to extend the nav for this page and call admin_externalpage_setup.
$pagesetup = function(string $pagetitle) {
global $PAGE;
navigation_node::override_active_url(
new moodle_url('/admin/settings.php', ['section' => 'enrolsettingslti_registrations'])
);
admin_externalpage_setup('enrolsettingslti_deployment_manage', '', null, '', ['pagelayout' => 'admin']);
$PAGE->navbar->add($pagetitle);
};
// Local anon helper to map the formdata to the dto required for the domain layer.
$maptodto = function($formdata): stdClass {
return (object) [
'registration_id' => $formdata->registrationid,
'deployment_name' => $formdata->name,
'deployment_id' => $formdata->deploymentid,
];
};
if ($action === 'add') {
$pagesetup(get_string('deploymentadd', 'enrol_lti'));
$pageurl = new moodle_url('/enrol/lti/manage_deployment.php', ['action' => 'add']);
$mform = new deployment_form($pageurl->out(false));
if ($data = $mform->get_data()) {
$deploymentservice = new tool_deployment_service(new application_registration_repository(),
new deployment_repository(), new resource_link_repository(), new context_repository(),
new user_repository());
$deploymentservice->add_tool_deployment($maptodto($data));
redirect($deploymentslisturl, get_string('deploymentaddnotice', 'enrol_lti'), null,
notification::NOTIFY_SUCCESS);
} else if (!$mform->is_cancelled()) {
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('deploymentadd', 'enrol_lti'));
$mform->set_data([
'registrationid' => $registrationid
]);
$mform->display();
echo $OUTPUT->footer();
die();
}
redirect($deploymentslisturl);
} else if ($action === 'delete') {
$id = required_param('id', PARAM_INT);
$pagesetup(get_string('deploymentdelete', 'enrol_lti'));
if (!optional_param('confirm', false, PARAM_BOOL)) {
$continueparams = [
'action' => 'delete',
'id' => $id,
'registrationid' => $registrationid,
'sesskey' => sesskey(),
'confirm' => true
];
$continueurl = new moodle_url('/enrol/lti/manage_deployment.php', $continueparams);
$deploymentrepo = new deployment_repository();
$deployment = $deploymentrepo->find($id);
if (!$deployment) {
throw new coding_exception("Cannot delete non existent deployment '{$id}'.");
}
echo $OUTPUT->header();
echo $OUTPUT->confirm(
get_string('deploymentdeleteconfirm', 'enrol_lti', format_string($deployment->get_deploymentid())),
$continueurl,
$deploymentslisturl
);
echo $OUTPUT->footer();
} else {
require_sesskey();
$deploymentservice = new tool_deployment_service(new application_registration_repository(),
new deployment_repository(), new resource_link_repository(), new context_repository(),
new user_repository());
$deploymentservice->delete_tool_deployment($id);
redirect($deploymentslisturl,
get_string('deploymentdeletenotice', 'enrol_lti'), null, notification::NOTIFY_SUCCESS);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5-103 385.5-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103zm128 1247v-190q0-14-9-23.5t-22-9.5h-192q-13 0-23 10t-10 23v190q0 13 10 23t23 10h192q13 0 22-9.5t9-23.5zm-2-344l18-621q0-12-10-18-10-8-24-8h-220q-14 0-24 8-10 6-10 18l17 621q0 10 10 17.5t24 7.5h185q14 0 23.5-7.5t10.5-17.5z" fill="#999"/></svg>

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1792 1248v320q0 40-28 68t-68 28h-320q-40 0-68-28t-28-68v-320q0-40 28-68t68-28h96v-192h-512v192h96q40 0 68 28t28 68v320q0 40-28 68t-68 28h-320q-40 0-68-28t-28-68v-320q0-40 28-68t68-28h96v-192h-512v192h96q40 0 68 28t28 68v320q0 40-28 68t-68 28h-320q-40 0-68-28t-28-68v-320q0-40 28-68t68-28h96v-192q0-52 38-90t90-38h512v-192h-96q-40 0-68-28t-28-68v-320q0-40 28-68t68-28h320q40 0 68 28t28 68v320q0 40-28 68t-68 28h-96v192h512q52 0 90 38t38 90v192h96q40 0 68 28t28 68z" fill="#888"/></svg>

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M888 1184l116-116-152-152-116 116v56h96v96h56zm440-720q-16-16-33 1l-350 350q-17 17-1 33t33-1l350-350q17-17 1-33zm80 594v190q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-14 14-32 8-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-126q0-13 9-22l64-64q15-15 35-7t20 29zm-96-738l288 288-672 672h-288v-288zm444 132l-92 92-288-288 92-92q28-28 68-28t68 28l152 152q28 28 28 68t-28 68z" fill="#888"/></svg>

After

Width:  |  Height:  |  Size: 703 B

+80
View File
@@ -0,0 +1,80 @@
<?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/>.
/**
* Tool proxy.
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
$toolid = null;
$token = null;
$filearguments = get_file_argument();
$arguments = explode('/', trim($filearguments, '/'));
if (count($arguments) == 2) {
list($toolid, $token) = $arguments;
}
$toolid = optional_param('id', $toolid, PARAM_INT);
$token = optional_param('token', $token, PARAM_ALPHANUM);
$PAGE->set_context(context_system::instance());
$url = new moodle_url('/enrol/lti/tp.php');
$PAGE->set_url($url);
$PAGE->set_pagelayout('popup');
$PAGE->set_title(get_string('registration', 'enrol_lti'));
// Only show the proxy if the token parameter is correct.
// If we do not compare with a shared secret, someone could very easily
// guess an id for the enrolment.
if (!\enrol_lti\helper::verify_proxy_token($toolid, $token)) {
throw new \moodle_exception('incorrecttoken', 'enrol_lti');
}
$tool = \enrol_lti\helper::get_lti_tool($toolid);
if (!is_enabled_auth('lti')) {
throw new \moodle_exception('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti'));
exit();
}
// Check if the enrolment plugin is disabled.
if (!enrol_is_enabled('lti')) {
throw new \moodle_exception('enrolisdisabled', 'enrol_lti');
exit();
}
// Check if the enrolment instance is disabled.
if ($tool->status != ENROL_INSTANCE_ENABLED) {
throw new \moodle_exception('enrolisdisabled', 'enrol_lti');
exit();
}
$messagetype = required_param('lti_message_type', PARAM_TEXT);
// Only accept proxy registration requests from this endpoint.
if ($messagetype != "ToolProxyRegistrationRequest") {
throw new \moodle_exception('invalidrequest', 'enrol_lti');
exit();
}
$toolprovider = new \enrol_lti\tool_provider($toolid);
$toolprovider->handleRequest();
echo $OUTPUT->header();
echo $OUTPUT->footer();
+198
View File
@@ -0,0 +1,198 @@
<?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/>.
/**
* LTI Advantage Initiate Dynamic Registration endpoint.
*
* https://www.imsglobal.org/spec/lti-dr/v1p0
*
* This endpoint handles the Registration Initiation Launch, in which a platform (via the user agent) sends their
* OpenID config URL and an optional registration token (to be used as the access token in the registration request).
*
* The code then makes the required dynamic registration calls, namely:
* 1. It fetches the platform's OpenID config by making a GET request to the provided OpenID config URL.
* 2. It then POSTS a client registration request (along with the registration token provided by the platform),
*
* Finally, the code returns to the user agent signalling a completed registration, via a HTML5 web message
* (postMessage). This lets the browser know the window may be closed.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core\context\system;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\context_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
use enrol_lti\local\ltiadvantage\service\application_registration_service;
require_once(__DIR__."/../../config.php");
global $OUTPUT, $PAGE, $CFG, $SITE;
require_once($CFG->libdir . '/filelib.php');
$PAGE->set_context(context_system::instance());
$PAGE->set_pagelayout('popup');
// URL to the platform's OpenID configuration.
$openidconfigurl = required_param('openid_configuration', PARAM_URL);
// Token generated by the platform, which must be sent back in registration request. This is opaque to the tool.
$regtoken = optional_param('registration_token', null, PARAM_RAW);
// Moodle-specific token used to secure the dynamic registration URL.
$token = required_param('token', PARAM_ALPHANUM);
$appregservice = new application_registration_service(
new application_registration_repository(),
new deployment_repository(),
new resource_link_repository(),
new context_repository(),
new user_repository()
);
// Using the application registration repo, find the incomplete registration using its unique id.
$appregrepo = new application_registration_repository();
$draftreg = $appregrepo->find_by_uniqueid($token);
if (is_null($draftreg) || $draftreg->is_complete()) {
throw new moodle_exception('invalidexpiredregistrationurl', 'enrol_lti');
}
// Get the OpenID config from the platform.
$curl = new curl();
$openidconfig = $curl->get($openidconfigurl);
$errno = $curl->get_errno();
if ($errno !== 0) {
throw new coding_exception("Error '{$errno}' while getting OpenID config from platform: {$openidconfig}");
}
$openidconfig = json_decode($openidconfig);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new moodle_exception('ltiadvdynregerror:invalidopenidconfigjson', 'enrol_lti');
}
$regendpoint = $openidconfig->registration_endpoint ?? null;
if (empty($regendpoint)) {
throw new coding_exception('Missing registration endpoint in OpenID configuration');
}
// Build the client registration request to send to the platform.
$wwwrooturl = $CFG->wwwroot;
$parsed = parse_url($wwwrooturl);
$sitefullname = format_string(get_site()->fullname);
$scopes = [
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly',
'https://purl.imsglobal.org/spec/lti-ags/scope/score',
'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly',
];
$regrequest = (object) [
'application_type' => 'web',
'grant_types' => ['client_credentials', 'implicit'],
'response_types' => ['id_token'],
'initiate_login_uri' => $CFG->wwwroot . '/enrol/lti/login.php?id=' . $draftreg->get_uniqueid(),
'redirect_uris' => [
$CFG->wwwroot . '/enrol/lti/launch.php',
$CFG->wwwroot . '/enrol/lti/launch_deeplink.php',
],
// TODO: Consider whether to support client_name#ja syntax for multi language support - see MDL-73109.
'client_name' => format_string($SITE->fullname, true, ['context' => system::instance()]),
'jwks_uri' => $CFG->wwwroot . '/enrol/lti/jwks.php',
'logo_uri' => $OUTPUT->get_compact_logo_url() ? $OUTPUT->get_compact_logo_url()->out(false) : '',
'token_endpoint_auth_method' => 'private_key_jwt',
'scope' => implode(" ", $scopes),
'https://purl.imsglobal.org/spec/lti-tool-configuration' => [
'domain' => $parsed['host'],
'target_link_uri' => $CFG->wwwroot . '/enrol/lti/launch.php',
'claims' => [
'iss',
'sub',
'aud',
'given_name',
'family_name',
'email',
'picture',
],
'messages' => [
(object) [
'type' => 'LtiDeepLinkingRequest',
'allowLearner' => false,
'target_link_uri' => $CFG->wwwroot . '/enrol/lti/launch_deeplink.php',
// TODO: Consider whether to support label#ja syntax for multi language support - see MDL-73109.
'label' => get_string('registrationdeeplinklabel', 'enrol_lti', $sitefullname),
'placements' => [
"ContentArea"
],
],
(object) [
'type' => 'LtiResourceLinkRequest',
'allowLearner' => true,
'target_link_uri' => $CFG->wwwroot . '/enrol/lti/launch.php',
// TODO: Consider whether to support label#ja syntax for multi language support - see MDL-73109.
'label' => get_string('registrationresourcelinklabel', 'enrol_lti', $sitefullname),
'placements' => [
"ContentArea"
],
],
]
]
];
if (!is_null($regtoken)) {
$curl->setHeader(['Authorization: Bearer ' . $regtoken]);
}
$curl->setHeader('Content-Type: application/json');
$regrequest = json_encode($regrequest);
$regresponse = $curl->post($regendpoint, $regrequest);
$errno = $curl->get_errno();
if ($errno !== 0) {
throw new coding_exception("Error '{$errno}' while posting client registration request to client: {$regresponse}");
}
if ($regresponse) {
$regresponse = json_decode($regresponse);
if ($regresponse->client_id) {
$toolconfig = $regresponse->{'https://purl.imsglobal.org/spec/lti-tool-configuration'};
if ($appregrepo->find_by_platform($openidconfig->issuer, $regresponse->client_id)) {
throw new moodle_exception('existingregistrationerror', 'enrol_lti');
}
// Registration of the tool on the platform was successful.
// Now update the platform details in the registration and mark it complete.
$draftreg->set_accesstokenurl(new moodle_url($openidconfig->token_endpoint));
$draftreg->set_authenticationrequesturl(new moodle_url($openidconfig->authorization_endpoint));
$draftreg->set_clientid($regresponse->client_id);
$draftreg->set_jwksurl(new moodle_url($openidconfig->jwks_uri));
$draftreg->set_platformid(new moodle_url($openidconfig->issuer));
$draftreg->complete_registration();
$appreg = $appregrepo->save($draftreg);
// Deployment id is optional.
// If this isn't provided by the platform at this time, it must be manually set in Site admin before launches can happen.
if (!empty($toolconfig->deployment_id)) {
$deployment = $appreg->add_tool_deployment($toolconfig->deployment_id, $toolconfig->deployment_id);
$deploymentrepo = new deployment_repository();
$deploymentrepo->save($deployment);
}
}
}
echo "<script>
(window.opener || window.parent).postMessage({subject: 'org.imsglobal.lti.close'}, '*');
</script>";
+176
View File
@@ -0,0 +1,176 @@
<?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/>.
/**
* LTI 1.3 page to create or edit a platform registration.
*
* This page is only used by LTI 1.3. Older versions do not require platforms to be registered with the tool during
* registration.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core\output\notification;
use enrol_lti\local\ltiadvantage\form\create_registration_form;
use enrol_lti\local\ltiadvantage\form\platform_registration_form;
use enrol_lti\local\ltiadvantage\entity\application_registration;
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
use enrol_lti\local\ltiadvantage\repository\context_repository;
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;
use enrol_lti\local\ltiadvantage\service\application_registration_service;
require_once(__DIR__ . '/../../config.php');
global $CFG, $OUTPUT, $DB;
require_once($CFG->libdir . '/adminlib.php');
require_once($CFG->dirroot . '/enrol/lti/lib.php');
$action = required_param('action', PARAM_ALPHA);
if (!in_array($action, ['add', 'view', 'edit', 'delete'])) {
throw new coding_exception("Invalid action param '$action'");
}
// The page to go back to when the respective action has been performed.
$toolregistrationurl = new moodle_url($CFG->wwwroot . "/" . $CFG->admin . "/settings.php",
['section' => 'enrolsettingslti_registrations']);
// Local anon helper to extend the nav for this page and call admin_externalpage_setup.
$pagesetup = function(string $pagetitle) {
global $PAGE;
navigation_node::override_active_url(
new moodle_url('/admin/settings.php', ['section' => 'enrolsettingslti_registrations'])
);
admin_externalpage_setup('enrolsettingslti_registrations_edit', '', null, '', ['pagelayout' => 'admin']);
$PAGE->navbar->add($pagetitle);
};
if ($action == 'view') {
$regid = required_param('regid', PARAM_INT);
$tabselect = optional_param('tabselect', 'platformdetails', PARAM_ALPHA);
global $PAGE;
$pagesetup(get_string('registerplatformedit', 'enrol_lti'));
$pageurl = new moodle_url('/enrol/lti/register_platform.php', ['action' => 'view', 'regid' => $regid]);
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('registerplatformedit', 'enrol_lti'));
$renderer = $PAGE->get_renderer('enrol_lti');
echo $renderer->render_registration_view($regid, $tabselect);
echo $OUTPUT->footer();
die();
} else if ($action === 'add') {
$pagesetup(get_string('registerplatformadd', 'enrol_lti'));
$pageurl = new moodle_url('/enrol/lti/register_platform.php', ['action' => 'add']);
$mform = new create_registration_form($pageurl->out(false));
if ($data = $mform->get_data()) {
// Create the incomplete registration.
$regservice = new application_registration_service(new application_registration_repository(),
new deployment_repository(), new resource_link_repository(), new context_repository(),
new user_repository());
$draft = $regservice->create_draft_application_registration($data);
// Redirect to the registration view, which will display endpoints and allow the user to complete the registration.
redirect(new moodle_url('/enrol/lti/register_platform.php',
['action' => 'view', 'regid' => $draft->get_id(), 'tabselect' => 'tooldetails']));
} else if (!$mform->is_cancelled()) {
// Display the first step of registration creation.
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('registerplatformadd', 'enrol_lti'));
$mform->display();
echo $OUTPUT->footer();
die();
}
redirect($toolregistrationurl);
} else if ($action === 'edit') {
$regid = required_param('regid', PARAM_INT);
$pagesetup(get_string('registerplatformedit', 'enrol_lti'));
$pageurl = new moodle_url('/enrol/lti/register_platform.php', ['action' => 'edit', 'regid' => $regid]);
$viewurl = new moodle_url('/enrol/lti/register_platform.php', ['action' => 'view', 'regid' => $regid]);
$mform = new platform_registration_form($pageurl->out(false));
if (($data = $mform->get_data()) && confirm_sesskey()) {
$regservice = new application_registration_service(new application_registration_repository(),
new deployment_repository(), new resource_link_repository(), new context_repository(),
new user_repository());
$regservice->update_application_registration($data);
redirect($viewurl, get_string('registerplatformeditnotice', 'enrol_lti'), null,
notification::NOTIFY_SUCCESS);
} else if (!$mform->is_cancelled()) {
// Anon helper to transform data.
$maptoformdata = function(application_registration $registration): \stdClass {
return (object) [
'id' => $registration->get_id(),
'name' => $registration->get_name(),
'platformid' => $registration->get_platformid(),
'clientid' => $registration->get_clientid(),
'authenticationrequesturl' => $registration->get_authenticationrequesturl(),
'jwksurl' => $registration->get_jwksurl(),
'accesstokenurl' => $registration->get_accesstokenurl()
];
};
$appregistrationrepo = new application_registration_repository();
$registration = $appregistrationrepo->find($regid);
if (!$registration) {
throw new coding_exception("cannot edit non-existent registration '{$regid}'.");
}
$mform->set_data($maptoformdata($registration));
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('registerplatformedit', 'enrol_lti'));
$mform->display();
echo $OUTPUT->footer();
die();
}
redirect($viewurl);
} else if ($action === 'delete') {
$regid = required_param('regid', PARAM_INT);
$pagesetup(get_string('registerplatformdelete', 'enrol_lti'));
if (!optional_param('confirm', false, PARAM_BOOL)) {
$continueparams = ['action' => 'delete', 'regid' => $regid, 'sesskey' => sesskey(), 'confirm' => true];
$continueurl = new moodle_url('/enrol/lti/register_platform.php', $continueparams);
$appregrepo = new application_registration_repository();
$appreg = $appregrepo->find($regid);
if (!$appreg) {
throw new coding_exception("Cannot delete non existent application registration '{$regid}'.");
}
echo $OUTPUT->header();
echo $OUTPUT->confirm(
get_string('registerplatformdeleteconfirm', 'enrol_lti', format_string($appreg->get_name())),
$continueurl,
$toolregistrationurl
);
echo $OUTPUT->footer();
} else {
require_sesskey();
$regservice = new application_registration_service(new application_registration_repository(),
new deployment_repository(), new resource_link_repository(), new context_repository(),
new user_repository());
$regservice->delete_application_registration($regid);
redirect($toolregistrationurl,
get_string('registerplatformdeletenotice', 'enrol_lti'), null, notification::NOTIFY_SUCCESS);
}
}
+105
View File
@@ -0,0 +1,105 @@
<?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/>.
/**
* General plugin functions.
*
* @package enrol_lti
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use enrol_lti\local\ltiadvantage\admin\admin_setting_registeredplatforms;
defined('MOODLE_INTERNAL') || die;
// The 'Publish as LTI tool' node is a category.
$ADMIN->add('enrolments', new admin_category('enrolltifolder', new lang_string('pluginname', 'enrol_lti'),
$this->is_enabled() === false));
$settings = new admin_settingpage($section, "User default values", 'moodle/site:config', $this->is_enabled() === false);
// Add all the user default values settings to the first page.
if ($ADMIN->fulltree) {
$settings->add(new admin_setting_heading('enrol_lti_settings', '', get_string('pluginname_desc', 'enrol_lti')));
if (!is_enabled_auth('lti')) {
$notify = new \core\output\notification(get_string('authltimustbeenabled', 'enrol_lti'),
\core\output\notification::NOTIFY_WARNING);
$settings->add(new admin_setting_heading('enrol_lti_enable_auth_lti', '', $OUTPUT->render($notify)));
}
if (empty($CFG->allowframembedding)) {
$notify = new \core\output\notification(get_string('allowframeembedding', 'enrol_lti'),
\core\output\notification::NOTIFY_WARNING);
$settings->add(new admin_setting_heading('enrol_lti_enable_embedding', '', $OUTPUT->render($notify)));
}
$settings->add(new admin_setting_heading('enrol_lti_user_default_values',
get_string('userdefaultvalues', 'enrol_lti'), ''));
$choices = array(0 => get_string('emaildisplayno'),
1 => get_string('emaildisplayyes'),
2 => get_string('emaildisplaycourse'));
$maildisplay = isset($CFG->defaultpreference_maildisplay) ? $CFG->defaultpreference_maildisplay : 2;
$settings->add(new admin_setting_configselect('enrol_lti/emaildisplay', get_string('emaildisplay'),
get_string('emaildisplay_help'), $maildisplay, $choices));
$city = '';
if (!empty($CFG->defaultcity)) {
$city = $CFG->defaultcity;
}
$settings->add(new admin_setting_configtext('enrol_lti/city', get_string('city'), '', $city));
$country = '';
if (!empty($CFG->country)) {
$country = $CFG->country;
}
$countries = array('' => get_string('selectacountry') . '...') + get_string_manager()->get_list_of_countries();
$settings->add(new admin_setting_configselect('enrol_lti/country', get_string('selectacountry'), '', $country,
$countries));
$settings->add(new admin_setting_configselect('enrol_lti/timezone', get_string('timezone'), '', 99,
core_date::get_list_of_timezones(null, true)));
$settings->add(new admin_setting_configselect('enrol_lti/lang', get_string('preferredlanguage'), '', $CFG->lang,
get_string_manager()->get_list_of_translations()));
$settings->add(new admin_setting_configtext('enrol_lti/institution', get_string('institution'), '', ''));
}
$ADMIN->add('enrolltifolder', $settings);
// Now, create a tool registrations settings page.
$settings = new admin_settingpage('enrolsettingslti_registrations', "Tool registration", 'moodle/site:config',
$this->is_enabled() === false);
$settings->add(new admin_setting_heading('enrol_lti_tool_registrations_heading',
get_string('registeredplatforms', 'enrol_lti'), ''));
$settings->add(new admin_setting_registeredplatforms());
$ADMIN->add('enrolltifolder', $settings);
// This adds a settings page to the 'publish as LTI tool' folder, hidden.
// On this page, we'll override the active node to force a match on enrolsettingslti_registrations settings page.
$ADMIN->add('enrolltifolder', new admin_externalpage('enrolsettingslti_registrations_edit',
get_string('registerplatformadd', 'enrol_lti'), "$CFG->wwwroot/$CFG->admin/enrol/lti/register_platform.php",
'moodle/site:config', true));
// And deployments add/edit.
$ADMIN->add('enrolltifolder', new admin_externalpage('enrolsettingslti_deployment_manage',
get_string('deployments', 'enrol_lti'), "$CFG->wwwroot/$CFG->admin/enrol/lti/manage_deployment.php",
'moodle/site:config', true));
// Tell core we're finished.
$settings = null;
+11
View File
@@ -0,0 +1,11 @@
/* enrol_lti styles */
#registration-tab-content .input-group-append button.btn {
border: solid 1px #8f959e;
}
form#auto_submit {
display: none;
}
#registration-tab-content .input-group-append button.btn span.pix {
padding-left: 0.5rem;
}
+56
View File
@@ -0,0 +1,56 @@
{{!
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/>.
}}
{{!
@template enrol_lti/copy_grid
The content to display when editing a tool.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* rows An array of objects with label, hidelabel, text and id
Example context (json):
{
"rows": [
{
"label": "Tool URL",
"text": "http://example.com/",
"id": "toolurl",
"hidelabel": false
},
{
"label": "Secret",
"text": "ABCDEF1234567890",
"id": "secret",
"hidelabel": true
}
]
}
}}
{{#rows}}
<div>
<label style="display: inline-block; width: 5em"
{{#id}}for="{{id}}-{{uniqid}}"{{/id}} {{#hidelabel}}
class="accesshide"{{/hidelabel}}>{{label}}</label>
<div style="display: inline-block">{{> core/copy_box }}</div>
</div>
{{/rows}}
@@ -0,0 +1,124 @@
{{!
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/>.
}}
{{!
@template enrol_lti/local/ltiadvantage/content_select
Template which displays a list of published courses and activities.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* hascontent
* action
* launchid
* sesskey
* courses
Example context (json):
{
"hascontent": true,
"action": "https://example.com/enrol/lti/configure.php",
"launchid": "123-456-789",
"sesskey": "123456789",
"courses": [
{
"id": 24,
"fullname": "Introduction to chemistry",
"shared_course": false,
"modules": [
{
"id": 45,
"name": "Quiz 1",
"lineitem": true
}
]
},
{
"id": 67,
"fullname": "Programming 101",
"shared_course": true
}
]
}
}}
<div id="lti_content_select">
<h1>{{#str}}publishedcontent, enrol_lti{{/str}}</h1>
{{#hascontent}}
<form action="{{action}}" method="POST">
<input type="hidden" name="launchid" value="{{launchid}}">
<input type="hidden" name="sesskey" value="{{sesskey}}">
<table class="table" aria-label="{{#str}}publishedcontent, enrol_lti{{/str}}">
<colgroup>
<col class="w-50">
<col class="w-25">
<col class="w-25">
</colgroup>
<thead>
<tr>
<th class="header" scope="col"></th>
<th class="header" scope="col">{{#str}}addtocourse, enrol_lti{{/str}}</th>
<th class="header" scope="col">{{#str}}addtogradebook, enrol_lti{{/str}}</th>
</tr>
</thead>
<tbody>
{{#courses}}
<tr>
<td>
{{fullname}}
</td>
<td>
{{#shared_course}}
<input type="checkbox" name="modules[]" value="{{id}}">
{{/shared_course}}
</td>
<td>
{{#shared_course}}
<input type="checkbox" name="grades[]" value="{{id}}">
{{/shared_course}}
</td>
</tr>
{{#modules}}
<tr>
<td>{{name}}</td>
<td><input type="checkbox" name="modules[]" value="{{id}}"></td>
{{#lineitem}}
<td><input type="checkbox" name="grades[]" value="{{id}}"></td>
{{/lineitem}}
{{^lineitem}}
<td>-</td>
{{/lineitem}}
</tr>
{{/modules}}
{{/courses}}
</tbody>
</table>
<input type="submit" class="btn btn-primary" value="{{#str}}addcontent, enrol_lti{{/str}}">
</form>
{{/hascontent}}
{{^hascontent}}
{{#str}}nopublishedcontent, enrol_lti{{/str}}
{{/hascontent}}
</div>
{{#js}}
require(['enrol_lti/content_select'], function(ContentSelect) {
ContentSelect.init();
});
{{/js}}
@@ -0,0 +1,50 @@
{{!
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/>.
}}
{{!
@template enrol_lti/local/ltiadvantage/cookies_required_notice
Displays a notice, reporting that cookies are required but couldn't be set.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* heading
* notification
Example context (json):
{
"heading": "Cookies are required",
"notification": {
"message": "You appear to be using an unsupported browser...",
"extraclasses": "",
"announce": true,
"closebutton": false,
"issuccess": false,
"isinfo": false,
"iswarning": true,
"iserror": false
}
}
}}
<h3>{{heading}}</h3>
{{#notification}}
{{> core/notification}}
{{/notification}}
@@ -0,0 +1,85 @@
{{!
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/>.
}}
{{!
@template enrol_lti/local/ltiadvantage/deployments
Template which displays a table view of all deployments attached to a registration, with options to delete and add.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* deployments_info
* has_deployments
* add_deployment_url
Optional context variables for this template:
* tool_deployments
Example context (json):
{
"deployments_info": "A deployment ID will be generated when...",
"has_deployments": true,
"tool_deployments": [
{
"name": "Site level deployment of tool x in platform y",
"deploymentid": "deploy-id-12345",
"deleteurl": "https://example.org/enrol/lti/manage_deployments.php?action=delete&id=2&registrationid=1"
}
],
"add_deployment_url": "https://example.org/enrol/lti/manage_deployments.php?action=add"
}
}}
<div id="lti_tool_deployments" class="mb-3">
<div class="alert alert-info alert-block">
{{{deployments_info}}}
</div>
{{#has_deployments}}
<table class="admintable generaltable">
<thead>
<tr>
<th>{{#str}}name, core{{/str}}</th>
<th>{{#str}}deploymentid, enrol_lti{{/str}}</th>
<th>{{#str}}actions, core{{/str}}</th>
</tr>
</thead>
<tbody>
{{#tool_deployments}}
<tr>
<th>{{name}}</th>
<td>{{deploymentid}}</td>
<td>
<a class="delete" href="{{deleteurl}}" title="{{#str}} delete {{/str}}">{{#pix}} t/delete, core, {{#str}} delete {{/str}}{{/pix}}</a>
</td>
</tr>
{{/tool_deployments}}
</tbody>
</table>
{{/has_deployments}}
{{^has_deployments}}
<div>
{{#str}}nodeployments, enrol_lti{{/str}}
</div>
<br>
{{/has_deployments}}
<div>
<a class="btn btn-secondary" href="{{add_deployment_url}}">{{#str}}deploymentadd, enrol_lti{{/str}}</a>
</div>
</div>
@@ -0,0 +1,69 @@
{{!
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/>.
}}
{{!
@template enrol_lti/local/ltiadvantage/platform_details
Template which displays a table containing platform configuration details.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* platform_details_info
* platform_details
* edit_platform_details_url
Example context (json):
{
"platform_details_info": "help text explaining where to get these details from, etc",
"platform_details": [
{
"name": "name",
"value": "Moodle LMS"
},
{
"name": "Platform ID",
"value": "https://lms.example.com/"
},
{
"name": "Client ID",
"value": "ab46f8ea123"
}
],
"edit_platform_details_url": "https://SITE/enrol/lti/register_platform.php?action=edit&id=x"
}
}}
<div class="alert alert-info alert-block">
{{{platform_details_info}}}
</div>
<table class="admintable generaltable">
<tbody>
{{#platform_details}}
<tr>
<th class="col-3">{{name}}</th>
<td class="col-9">
{{#value}}{{.}}{{/value}}
{{^value}}-{{/value}}
</td>
</tr>
{{/platform_details}}
</tbody>
</table>
<a class="btn btn-secondary" href="{{edit_platform_details_url}}">{{#str}}editplatformdetails, enrol_lti{{/str}}</a>
@@ -0,0 +1,110 @@
{{!
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/>.
}}
{{!
@template enrol_lti/local/ltiadvantage/registered_platforms
Template which displays a table view of all registered apps with options to add, delete and manage deployments.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* hasregs
* addurl
Optional context variables for this template:
* registrations
Example context (json):
{
"hasregs": true,
"registrations": [
{
"name": "Platform Y registration",
"hasdeployments": true,
"countdeployments": 2,
"editurl": "https://example.org/enrol/lti/register_platform.php?action=edit&regid=4",
"deploymentsurl": "https://example.org/enrol/lti/manage_deployments.php?registrationid=1",
"deleteurl": "https://example.org/enrol/lti/register_platform.php?action=delete&regid=4"
}
],
"addurl": "https://example.org/enrol/lti/manage_deployments.php?action=add"
}
}}
<div id="lti_registered_platforms" class="mb-3">
{{#hasregs}}
<table class="admintable generaltable">
<thead>
<tr>
<th>{{#str}}name, core{{/str}}</th>
<th>{{#str}}details, enrol_lti{{/str}}</th>
<th>{{#str}}deployments, enrol_lti{{/str}}</th>
<th>{{#str}}registrationstatus, enrol_lti{{/str}}</th>
<th>{{#str}}actions, core{{/str}}</th>
</tr>
</thead>
<tbody>
{{#registrations}}
<tr>
<th>
{{name}}
</th>
<td>
<span class="font-weight-bold">{{#str}}registerplatform:platformid, enrol_lti{{/str}}: </span> {{issuer}}<br>
<span class="font-weight-bold">{{#str}}registerplatform:clientid, enrol_lti{{/str}}: </span> {{clientid}}
</td>
<td>
{{#hasdeployments}}
<a href="{{deploymentsurl}}" title="{{#str}} managedeployments, enrol_lti {{/str}}">{{countdeployments}}</a>
{{/hasdeployments}}
{{^hasdeployments}}
{{countdeployments}}
{{/hasdeployments}}
</td>
<td>
{{#isactive}}
<span class="badge bg-success text-white">{{{statusstring}}}</span>
{{/isactive}}
{{^isactive}}
<span class="badge bg-info text-white">{{{statusstring}}}</span>
{{/isactive}}
</td>
<td>
<a href="{{tooldetailsurl}}" title="{{#str}} viewtoolendpoints, enrol_lti {{/str}}">{{#pix}} t/viewdetails, core, {{#str}} viewtoolendpoints, enrol_lti {{/str}}{{/pix}}</a>
<a href="{{platformdetailsurl}}" title="{{#str}} viewplatformdetails, enrol_lti {{/str}}">{{#pix}} platformdetails, enrol_lti, {{#str}} viewplatformdetails, enrol_lti {{/str}}{{/pix}}</a>
<a href="{{deploymentsurl}}" title="{{#str}} managedeployments, enrol_lti {{/str}}">{{#pix}} managedeployments, enrol_lti, {{#str}} managedeployments, enrol_lti {{/str}}{{/pix}}</a>
<a href="{{deleteurl}}" title="{{#str}} delete {{/str}}">{{#pix}} t/delete, core, {{#str}} delete {{/str}}{{/pix}}</a>
</td>
</tr>
{{/registrations}}
</tbody>
</table>
{{/hasregs}}
{{^hasregs}}
<div>
{{#str}}noregisteredplatforms, enrol_lti{{/str}}
</div>
<br>
{{/hasregs}}
<div>
<a class="btn btn-secondary" href="{{addurl}}">{{#str}}registerplatformadd, enrol_lti{{/str}}</a>
</div>
</div>
@@ -0,0 +1,114 @@
{{!
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/>.
}}
{{!
@template enrol_lti/local/ltiadvantage/registration_view
Template which displays details about a registration, allowing users to view the dynamic registration URL, the manual
registration URLs and tool deployments. All the URLs support copy to clipboard, included in the
enrol_lti/local/ltiadvantage/tool_endpoints.mustache file.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* dynamic_registration_info
* dynamic_registration_url
* manual_registration_info
* manual_registration_urls
* platform_details_info
* platform_details
* edit_platform_details_url
* deployments_info
* has_deployments
Optional context variables for this template:
* tool_deployments
Example context (json):
{
"dynamic_registration_info": "Help/information about dynamic registration",
"dynamic_registration_url": {
"id": "ab34dgdfa",
"name": "Dynamic registration URL",
"url": "https://example.com/enrol/lti/register.php?tok=asdb123b12babsdb"
},
"manual_registration_info": "Help/information about manual registration",
"manual_registration_urls": [
{
"id": "asdb1234m",
"name": "Authentication Request URL",
"url": "https://example.com/enrol/lti/login.php?id=a2c94"
},
{
"id": "123GHn123",
"name": "JWKS URL",
"url": "https://example.com/enrol/lti/jwks.php"
}
],
"platform_details_info": "Once the tool has been set up in the platform, details from the platform must...",
"platform_details": {
"name": "My LMS",
"platformid": "https://lms.example.com",
"clientid": "a265bcd3a8f5bcd",
"authenticationrequesturl": "https://lms.example.com/auth",
"jwksurl": "https://lms.example.com/jwks",
"accesstokenurl": "https://lms.example.com/token"
},
"edit_platform_details_url": "https://SITE/enrol/lti/register_platform.php?action=edit&regid=xx",
"deployments_info": "A deployment ID will be generated when...",
"has_deployments": true,
"tool_deployments": [
{
"name": "Site level deployment of tool x in platform y",
"deploymentid": "deploy-id-12345",
"deleteurl": "https://example.org/enrol/lti/manage_deployments.php?action=delete&id=2&registrationid=1"
}
]
}
}}
<a class="btn btn-secondary mb-3" href="{{back_url}}">{{#str}}back, core{{/str}}</a>
<ul class="nav nav-tabs mb-3" id="registration-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link {{#tool_details_active}}active{{/tool_details_active}}" id="tool-details-tab" data-toggle="tab" href="#tooldetails" role="tab" aria-controls="tooldetails" aria-selected="{{#tool_details_active}}true{{/tool_details_active}}{{^tool_details_active}}false{{/tool_details_active}}">
{{#str}}tooldetails, enrol_lti{{/str}}
</a>
</li>
<li class="nav-item">
<a class="nav-link {{#platform_details_active}}active{{/platform_details_active}}" id="platform-details-tab" data-toggle="tab" href="#platformdetails" role="tab" aria-controls="platformdetails" aria-selected="{{#platform_details_active}}true{{/platform_details_active}}{{^platform_details_active}}false{{/platform_details_active}}">
{{#str}}platformdetails, enrol_lti{{/str}}
</a>
</li>
<li class="nav-item">
<a class="nav-link {{#tool_deployments_active}}active{{/tool_deployments_active}}" id="tool-deployments-tab" data-toggle="tab" href="#tooldeployments" role="tab" aria-controls="tooldeployments" aria-selected="{{#tool_deployments_active}}true{{/tool_deployments_active}}{{^tool_deployments_active}}false{{/tool_deployments_active}}">
{{#str}}deployments, enrol_lti{{/str}}
</a>
</li>
</ul>
<div class="tab-content" id="registration-tab-content">
<div class="tab-pane fade {{#tool_details_active}}show active{{/tool_details_active}}" id="tooldetails" role="tabpanel" aria-labelledby="tool-details-tab">
{{> enrol_lti/local/ltiadvantage/tool_details}}
</div>
<div class="tab-pane fade {{#platform_details_active}}show active{{/platform_details_active}}" id="platformdetails" role="tabpanel" aria-labelledby="platform-details-tab">
{{> enrol_lti/local/ltiadvantage/platform_details}}
</div>
<div class="tab-pane fade {{#tool_deployments_active}}show active{{/tool_deployments_active}}" id="tooldeployments" role="tabpanel" aria-labelledby="tool-deployments-tab">
{{> enrol_lti/local/ltiadvantage/deployments}}
</div>
</div>
@@ -0,0 +1,132 @@
{{!
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/>.
}}
{{!
@template enrol_lti/local/ltiadvantage/tool_details
Template which displays a list of tool endpoint URLs, allowing the values to be copied to the clipboard via js.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* dynamic_registration_info
* dynamic_registration_url
* manual_registration_info
* manual_registration_urls
Example context (json):
{
"dynamic_registration_info": "Help text describing dynamic registration...",
"dynamic_registration_url": {
"id": "ab34dgdfa",
"name": "Dynamic registration URL",
"url": "https://example.com/enrol/lti/register.php?tok=asdb123b12babsdb"
},
"manual_registration_info": "Help text describing manual registration...",
"manual_registration_urls": [
{
"id": "asdb1234m",
"name": "Authentication Request URL",
"url": "https://example.com/enrol/lti/login.php"
},
{
"id": "123GHn123",
"name": "JWKS URL",
"url": "https://example.com/enrol/lti/jwks.php"
}
]
}
}}
<h5>{{#str}}registrationdynamic, enrol_lti{{/str}}</h5>
<div class="alert alert-info alert-block">
{{{dynamic_registration_info}}}
</div>
{{#dynamic_registration_url}}
<table class="admintable generaltable">
<thead></thead>
<tbody>
<tr class="d-flex">
<th class="col-3" id="lti_tool_endpoint_name_{{id}}">
<label for="lti_tool_endpoint_url_{{id}}">{{name}}</label>
</th>
<td class="col-9">
<div class="input-group col-md-6 ml-0 pl-0">
<input class="form-control" type="text" value="{{url}}" aria-label="{{url}}" id="lti_tool_endpoint_url_{{id}}" readonly>
<div class="input-group-append">
<button class="btn btn-secondary"
aria-label="{{#str}} copytoclipboard, enrol_lti {{/str}}"
data-action="copytoclipboard"
data-clipboard-success-message="{{#str}}copiedtoclipboard, enrol_lti, {{name}}{{/str}}"
data-clipboard-target="#lti_tool_endpoint_url_{{id}}"
id="{{id}}"
title="{{#str}}copytoclipboard, enrol_lti{{/str}}">
<span class="pix">{{#pix}} t/clipboard, core {{/pix}}</span>
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
{{/dynamic_registration_url}}
<h5>{{#str}}registrationmanual, enrol_lti{{/str}}</h5>
<div class="alert alert-info alert-block">
{{manual_registration_info}}
</div>
<div id="lti_tool_endpoints">
<table class="admintable generaltable">
<thead>
<tr class="d-flex">
<th>{{#str}}name, core{{/str}}</th>
<th>{{#str}}url, core{{/str}}</th>
</tr>
</thead>
<tbody>
{{#manual_registration_urls}}
<tr class="d-flex">
<th class="col-3" id="lti_tool_endpoint_name_{{id}}">
<label for="lti_tool_endpoint_url_{{id}}">{{name}}</label>
</th>
<td class="col-9">
<div class="input-group col-md-6 ml-0 pl-0">
<input class="form-control" type="text" value="{{url}}" aria-label="{{url}}" id="lti_tool_endpoint_url_{{id}}" readonly>
<div class="input-group-append">
<button class="btn btn-secondary"
aria-label="{{#str}} copytoclipboard, enrol_lti {{/str}}"
data-action="copytoclipboard"
data-clipboard-success-message="{{#str}}copiedtoclipboard, enrol_lti, {{name}}{{/str}}"
data-clipboard-target="#lti_tool_endpoint_url_{{id}}"
id="{{id}}"
title="{{#str}}copytoclipboard, enrol_lti{{/str}}">
<span class="pix">{{#pix}} t/clipboard, core {{/pix}}</span>
</button>
</div>
</div>
</td>
</tr>
{{/manual_registration_urls}}
</tbody>
</table>
</div>
{{#js}}
require(['enrol_lti/tool_endpoints'], function(ToolEndpoints) {
ToolEndpoints.init();
});
{{/js}}
@@ -0,0 +1,38 @@
{{!
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/>.
}}
{{!
@template enrol_lti/proxy_registration
The content to display when editing a tool.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* returnurl The url to return the page to. Can be null if no return url avaliable.
Example context (json):
{}
}}
{{#str}} successfulregistration, enrol_lti {{/str}}
<br/>
{{#returnurl}}
<a href="{{{returnurl}}}">{{#str}} continue {{/str}}</a>
{{/returnurl}}
@@ -0,0 +1,53 @@
@enrol @enrol_lti
Feature: Check that settings are adhered to when creating an enrolment plugin
In order to create an LTI enrolment instance
As an admin
I need to ensure the site-wide settings are used
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "admin"
And I navigate to "Plugins > Enrolments > Manage enrol plugins" in site administration
And I click on "Enable" "link" in the "Publish as LTI tool" "table_row"
And I navigate to "Plugins > Enrolments > Publish as LTI tool" in site administration
And I set the following fields to these values:
| Email visibility | Visible to everyone |
| City/town | Perth |
| Select a country | Australia |
| Timezone | Australia/Perth |
| Institution | Moodle Pty Ltd |
And I press "Save changes"
And I log out
Scenario: As an admin set site-wide settings for the enrolment plugin and ensure they are used
Given I log in as "teacher1"
And I am on the "Course 1" "enrolment methods" page
And I select "Publish as LTI tool" from the "Add method" singleselect
When I expand all fieldsets
Then the field "Email visibility" matches value "Visible to everyone"
And the field "City/town" matches value "Perth"
And the field "Select a country" matches value "Australia"
And the field "Timezone" matches value "Australia/Perth"
And the field "Institution" matches value "Moodle Pty Ltd"
And I set the following fields to these values:
| Email visibility | Hidden |
| City/town | Whistler |
| Select a country | Canada |
| Timezone | America/Vancouver |
| Institution | Moodle Pty Ltd - remote |
And I press "Add method"
And I click on "Edit" "link" in the "Publish as LTI tool" "table_row"
And the field "Email visibility" matches value "Hidden"
And the field "City/town" matches value "Whistler"
And the field "Select a country" matches value "Canada"
And the field "Timezone" matches value "America/Vancouver"
And the field "Institution" matches value "Moodle Pty Ltd - remote"
+55
View File
@@ -0,0 +1,55 @@
@enrol @enrol_lti
Feature: Check that the page listing the shared external tools is functioning as expected
In order to edit an external tool
As a teacher
I need to ensure the tool listing page is working as expected
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Test assignment name | Submit your online text | C1 | assign1 | 1 |
And I log in as "admin"
And I navigate to "Plugins > Enrolments > Manage enrol plugins" in site administration
And I click on "Enable" "link" in the "Publish as LTI tool" "table_row"
And I log out
Scenario: I want to edit an external tool
Given I log in as "teacher1"
And I turn editing mode on
And I am on the "Course 1" "enrolment methods" page
And I select "Publish as LTI tool" from the "Add method" singleselect
And I set the following fields to these values:
| Custom instance name | Assignment - LTI |
| Tool to be published | Test assignment name |
| LTI version | Legacy LTI (1.1/2.0) |
And I press "Add method"
And I am on "Course 1" course homepage
And I navigate to "Published as LTI tools" in current page administration
And I click on "Legacy LTI (1.1/2.0" "link"
And I should see "Assignment - LTI" in the ".generaltable" "css_element"
When I click on "Disable" "link" in the "Assignment - LTI" "table_row"
Then ".dimmed_text" "css_element" should exist in the "Assignment - LTI" "table_row"
And I click on "Enable" "link" in the "Assignment - LTI" "table_row"
And ".dimmed_text" "css_element" should not exist in the "Assignment - LTI" "table_row"
And I click on "Edit" "link" in the "Assignment - LTI" "table_row"
And I set the following fields to these values:
| Custom instance name | Course - LTI |
| Tool to be published | Course |
And I press "Save changes"
And I should see "Course - LTI" in the ".generaltable" "css_element"
And I click on "Delete" "link" in the "Course - LTI" "table_row"
And I press "Cancel"
And I should see "Course - LTI" in the ".generaltable" "css_element"
And I click on "Delete" "link" in the "Course - LTI" "table_row"
And I press "Continue"
And I should see "No resources or activities are published yet"
And I should not see "Course - LTI"
@@ -0,0 +1,105 @@
@enrol @enrol_lti
Feature: Publish activities and resources over LTI Advantage
In order to make content available to external platforms
As a teacher
I need to be able to publish and manage activities and resources using LTI Advantage
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Test assignment name | Submit your online text | C1 | assign1 | 1 |
And I enable "lti" "enrol" plugin
Scenario: A teacher can publish an activity
Given I log in as "teacher1"
And I am on the "Course 1" "enrolment methods" page
When I select "Publish as LTI tool" from the "Add method" singleselect
And the following fields match these values:
| LTI version | LTI Advantage |
And I set the following fields to these values:
| Custom instance name | Published assignment |
| Tool to be published | Test assignment name |
And I press "Add method"
And I am on "Course 1" course homepage
And I navigate to "Published as LTI tools" in current page administration
Then I should see "Launch URL" in the "Published assignment" "table_row"
And I should see "Custom properties" in the "Published assignment" "table_row"
And "Edit" "link" should exist in the "Published assignment" "table_row"
And "Delete" "link" should exist in the "Published assignment" "table_row"
And "Disable" "link" should exist in the "Published assignment" "table_row"
Scenario: A teacher can edit a published resource/activity
Given the following "enrol_lti > published resources" exist:
| name | uuid | activity | course |
| Published assignment | my-uuid-123 | assign1 | C1 |
And I am on the "Course 1" "Course" page logged in as "teacher1"
And I navigate to "Published as LTI tools" in current page administration
And the "value" attribute of "Custom properties" "field" should contain "id=my-uuid-123"
When I click on "Edit" "link" in the "Published assignment" "table_row"
And I set the following fields to these values:
| Custom instance name | New instance name |
And I press "Cancel"
And I should see "Published assignment" in the "Published assignment" "table_row"
And the "value" attribute of "Custom properties" "field" should contain "id=my-uuid-123"
And I click on "Edit" "link" in the "Published assignment" "table_row"
And I set the following fields to these values:
| Custom instance name | New instance name |
And I press "Save changes"
Then I should see "New instance name"
And the "value" attribute of "Custom properties" "field" should contain "id=my-uuid-123"
Scenario: A teacher can disable and enable a published resource/activity
Given the following "enrol_lti > published resources" exist:
| name | activity | course |
| Published assignment | assign1 | C1 |
And I am on the "Course 1" "Course" page logged in as "teacher1"
And I navigate to "Published as LTI tools" in current page administration
When I click on "Disable" "link" in the "Published assignment" "table_row"
Then ".dimmed_text" "css_element" should exist in the "Published assignment" "table_row"
And I click on "Enable" "link" in the "Published assignment" "table_row"
And ".dimmed_text" "css_element" should not exist in the "Published assignment" "table_row"
Scenario: A teacher can delete a published tool
Given the following "enrol_lti > published resources" exist:
| name | activity | course |
| Published assignment | assign1 | C1 |
And I am on the "Course 1" "Course" page logged in as "teacher1"
And I navigate to "Published as LTI tools" in current page administration
When I click on "Delete" "link" in the "Published assignment" "table_row"
And I press "Cancel"
And I should see "Published assignment" in the "Published assignment" "table_row"
And I click on "Delete" "link" in the "Published assignment" "table_row"
And I press "Continue"
And I should see "No resources or activities are published yet"
And I should not see "Published assignment"
Scenario: A teacher can switch the version of a published resource from LTI 1.1 to LTI Advantage
Given the following "enrol_lti > published resources" exist:
| name | activity | course | ltiversion |
| Published assignment | assign1 | C1 | LTI-1p0/LTI-2p0 |
And I am on the "Course 1" "Course" page logged in as "teacher1"
And I navigate to "Published as LTI tools" in current page administration
And I should see "No resources or activities are published yet"
And I click on "Legacy LTI (1.1/2.0)" "link"
And I should see "Published assignment"
When I click on "Edit" "link" in the "Published assignment" "table_row"
And the following fields match these values:
| LTI version | Legacy LTI (1.1/2.0) |
And I set the following fields to these values:
| LTI version | LTI Advantage |
| Custom instance name | New instance name |
And I press "Save changes"
And I click on "LTI Advantage" "link"
Then I should see "New instance name"
And "LTI Advantage" "link" should not exist
And "Legacy LTI (1.1/2.0)" "link" should exist
And the "value" attribute of "Custom properties" "field" should contain "id="
@@ -0,0 +1,215 @@
@enrol @enrol_lti
Feature: Register a platform with the tool
In order to share and consume a Moodle resource or activity over LTI Advantage
As an admin
I need to be able to manage platform registrations in the tool
Background:
Given I enable "lti" "enrol" plugin
Scenario: An admin can register a platform with the tool
Given I log in as "admin"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
When I follow "Register a platform"
And I set the following fields to these values:
| Platform name | My test platform |
And I press "Continue"
And I should see "Dynamic registration"
And I should see "Manual registration"
And I should see "Platform details"
And I follow "Back"
And "Manage deployments" "link" should exist in the "My test platform" "table_row"
And "View platform details" "link" should exist in the "My test platform" "table_row"
And "Delete" "link" should exist in the "My test platform" "table_row"
And I should see "Pending" in the "My test platform" "table_row"
And I click on "View platform details" "link" in the "My test platform" "table_row"
And I follow "Edit platform details"
And I set the following fields to these values:
| Platform ID (issuer) | https://lms.example.com |
| Client ID | abcd1234 |
| Authentication request URL | https://lms.example.com/auth |
| Public keyset URL | https://lms.example.com/jwks |
| Access token URL | https://lms.example.com/token |
And I press "Save changes"
Then I should see "Platform registration updated"
And I should see "My test platform" in the "Platform name" "table_row"
And I should see "https://lms.example.com" in the "Platform ID (issuer)" "table_row"
And I should see "abcd1234" in the "Client ID" "table_row"
And I should see "https://lms.example.com/auth" in the "Authentication request URL" "table_row"
And I should see "https://lms.example.com/jwks" in the "Public keyset URL" "table_row"
And I should see "https://lms.example.com/token" in the "Access token URL" "table_row"
And I follow "Back"
And I should see "https://lms.example.com" in the "My test platform" "table_row"
And I should see "Active" in the "My test platform" "table_row"
And "View platform details" "link" should exist in the "My test platform" "table_row"
And "Manage deployments" "link" should exist in the "My test platform" "table_row"
And "Delete" "link" should exist in the "My test platform" "table_row"
Scenario: An admin can edit a platform's registration details
Given the following "enrol_lti > application registrations" exist:
| name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl |
| My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token |
And I log in as "admin"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
When I click on "View platform details" "link" in the "My test platform" "table_row"
And I follow "Edit platform details"
And I set the following fields to these values:
| Platform name | Changed test platform |
| Platform ID (issuer) | https://lms2.example.com |
| Client ID | wxyz9876 |
| Authentication request URL | https://lms2.example.com/auth |
| Public keyset URL | https://lms2.example.com/jwks |
| Access token URL | https://lms2.example.com/token |
And I press "Cancel"
Then I should see "https://lms.example.com" in the "Platform ID (issuer)" "table_row"
And I follow "Edit platform details"
And the following fields match these values:
| Platform name | My test platform |
| Platform ID (issuer) | https://lms.example.com |
| Client ID | abcd1234 |
| Authentication request URL | https://lms.example.com/auth |
| Public keyset URL | https://lms.example.com/jwks |
| Access token URL | https://lms.example.com/token |
And I set the following fields to these values:
| Platform name | Changed test platform |
| Platform ID (issuer) | https://lms2.example.com |
| Client ID | wxyz9876 |
| Authentication request URL | https://lms2.example.com/auth |
| Public keyset URL | https://lms2.example.com/jwks |
| Access token URL | https://lms2.example.com/token |
And I press "Save changes"
And I should see "https://lms2.example.com" in the "Platform ID (issuer)" "table_row"
And I follow "Edit platform details"
And the following fields match these values:
| Platform name | Changed test platform |
| Platform ID (issuer) | https://lms2.example.com |
| Client ID | wxyz9876 |
| Authentication request URL | https://lms2.example.com/auth |
| Public keyset URL | https://lms2.example.com/jwks |
| Access token URL | https://lms2.example.com/token |
And I press "Cancel"
And I follow "Back"
And I should see "https://lms2.example.com" in the "Changed test platform" "table_row"
Scenario: An admin can delete a platform registration
Given the following "enrol_lti > application registrations" exist:
| name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl |
| My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token |
And I log in as "admin"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
When I click on "Delete" "link" in the "My test platform" "table_row"
And I press "Cancel"
Then I should see "https://lms.example.com" in the "My test platform" "table_row"
And I click on "Delete" "link" in the "My test platform" "table_row"
And I press "Continue"
And I should see "Platform registration deleted"
And I should not see "My test platform"
Scenario: A platform registration's unique platformid:clientid tuple must be enforced
Given the following "enrol_lti > application registrations" exist:
| name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl |
| My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token |
And I log in as "admin"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
When I follow "Register a platform"
And I set the following fields to these values:
| Platform name | My test platform |
And I press "Continue"
And I follow "Edit platform details"
And I set the following fields to these values:
| Platform name | My test platform |
| Platform ID (issuer) | https://lms.example.com |
| Client ID | abcd1234 |
| Authentication request URL | https://lms.example.com/auth |
| Public keyset URL | https://lms.example.com/jwks |
| Access token URL | https://lms.example.com/token |
And I press "Save changes"
Then I should see "Invalid client ID. This client ID is already registered for the platform ID provided."
Scenario: An admin can add deployment ids for a given platform registration
Given the following "enrol_lti > application registrations" exist:
| name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl |
| My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token |
And I log in as "admin"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
When I click on "Manage deployments" "link" in the "My test platform" "table_row"
And I should see "No tool deployments found"
And I follow "Add a deployment"
And I set the following fields to these values:
| Deployment name | Sitewide deployment of Moodle on platform x |
| Deployment ID | 1a2b3c |
And I press "Cancel"
And I should not see "Sitewide deployment of Moodle on platform x"
And I should see "No tool deployments found"
And I follow "Add a deployment"
And I set the following fields to these values:
| Deployment name | Sitewide deployment of Moodle on platform x |
| Deployment ID | 1a2b3c |
And I press "Save changes"
Then I should see "Deployment added"
And I should see "1a2b3c" in the "Sitewide deployment of Moodle on platform x" "table_row"
And I should not see "No tool deployments found"
And "Delete" "link" should exist in the "Sitewide deployment of Moodle on platform x" "table_row"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
And "1" "link" should exist in the "My test platform" "table_row"
And I click on "1" "link" in the "My test platform" "table_row"
And I should see "1a2b3c" in the "Sitewide deployment of Moodle on platform x" "table_row"
And I follow "Add a deployment"
And I set the following fields to these values:
| Deployment name | Course context deployment of Moodle on platform x |
| Deployment ID | 4d5e6f |
And I press "Save changes"
And I should see "4d5e6f" in the "Course context deployment of Moodle on platform x" "table_row"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
And "2" "link" should exist in the "My test platform" "table_row"
Scenario: An admin can remove a deployment id for a platform registration
Given the following "enrol_lti > application registrations" exist:
| name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl | deploymentname | deploymentid |
| My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token | Site deployment | 1a2b3c |
And I log in as "admin"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
And I should see "1" in the "My test platform" "table_row"
And I click on "Manage deployments" "link" in the "My test platform" "table_row"
And I should see "1a2b3c" in the "Site deployment" "table_row"
When I click on "Delete" "link" in the "Site deployment" "table_row"
And I press "Cancel"
Then I should see "1a2b3c" in the "Site deployment" "table_row"
And I click on "Delete" "link" in the "Site deployment" "table_row"
And I press "Continue"
And I should see "Deployment deleted"
And I should see "No tool deployments found"
And I should not see "1a2b3c"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
And "1" "link" should not exist in the "My test platform" "table_row"
@javascript
Scenario: An admin can copy the manual and dynamic registration endpoints to register the tool with the platform
Given the following "enrol_lti > application registrations" exist:
| name |
| My test platform |
And I log in as "admin"
And I change window size to "large"
And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration
When I click on "View platform details" "link" in the "My test platform" "table_row"
And I follow "Tool details"
And the "value" attribute of "Registration URL" "field" should contain "enrol/lti/register.php"
And the "value" attribute of "Tool URL" "field" should contain "enrol/lti/launch.php"
And the "value" attribute of "Initiate login URL" "field" should contain "enrol/lti/login.php"
And the "value" attribute of "JWKS URL" "field" should contain "enrol/lti/jwks.php"
And the "value" attribute of "Deep linking URL" "field" should contain "enrol/lti/launch_deeplink.php"
And "Copy to clipboard" "button" should exist in the "Registration URL" "table_row"
And "Copy to clipboard" "button" should exist in the "Tool URL" "table_row"
And "Copy to clipboard" "button" should exist in the "Initiate login URL" "table_row"
And "Copy to clipboard" "button" should exist in the "JWKS URL" "table_row"
And "Copy to clipboard" "button" should exist in the "Deep linking URL" "table_row"
When I click on "Copy to clipboard" "button" in the "Registration URL" "table_row"
Then I should see "Registration URL copied to clipboard"
And I click on "Copy to clipboard" "button" in the "Tool URL" "table_row"
And I should see "Tool URL copied to clipboard"
And I click on "Copy to clipboard" "button" in the "Initiate login URL" "table_row"
And I should see "Initiate login URL copied to clipboard"
And I click on "Copy to clipboard" "button" in the "JWKS URL" "table_row"
And I should see "JWKS URL copied to clipboard"
And I click on "Copy to clipboard" "button" in the "Deep linking URL" "table_row"
And I should see "Deep linking URL copied to clipboard"
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<firstnode></firstnode>
<parentnode>
<childnode></childnode>
</parentnode>
<ambiguous id="0"></ambiguous>
<ambiguous id="1"></ambiguous>
</root>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<firstnode/>
<parentnode>
<childnode/>
</parentnode>
<ambiguous id="0"/>
<ambiguous id="1">Content 1</ambiguous>
</root>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<firstnode>Content 1</firstnode>
<parentnode>
<childnode>Content 2</childnode>
</parentnode>
<ambiguous id="0"/>
<ambiguous id="1"/>
</root>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<firstnode>Content 1</firstnode>
<parentnode>
<childnode/>
</parentnode>
<ambiguous id="0"/>
<ambiguous id="1"/>
</root>

Some files were not shown because too many files have changed in this diff Show More