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