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
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);
}
}
}