See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Handles synchronising members using the enrolment LTI. 19 * 20 * @package enrol_lti 21 * @copyright 2016 Mark Nelson <markn@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace enrol_lti\task; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 use core\task\scheduled_task; 30 use core_user; 31 use enrol_lti\data_connector; 32 use enrol_lti\helper; 33 use IMSGlobal\LTI\ToolProvider\Context; 34 use IMSGlobal\LTI\ToolProvider\ResourceLink; 35 use IMSGlobal\LTI\ToolProvider\ToolConsumer; 36 use IMSGlobal\LTI\ToolProvider\User; 37 use stdClass; 38 39 require_once($CFG->dirroot . '/user/lib.php'); 40 41 /** 42 * Task for synchronising members using the enrolment LTI. 43 * 44 * @package enrol_lti 45 * @copyright 2016 Mark Nelson <markn@moodle.com> 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 */ 48 class sync_members extends scheduled_task { 49 50 /** @var array Array of user photos. */ 51 protected $userphotos = []; 52 53 /** @var data_connector $dataconnector A data_connector instance. */ 54 protected $dataconnector; 55 56 /** 57 * Get a descriptive name for this task. 58 * 59 * @return string 60 */ 61 public function get_name() { 62 return get_string('tasksyncmembers', 'enrol_lti'); 63 } 64 65 /** 66 * Performs the synchronisation of members. 67 */ 68 public function execute() { 69 if (!is_enabled_auth('lti')) { 70 mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti'))); 71 return; 72 } 73 74 // Check if the enrolment plugin is disabled - isn't really necessary as the task should not run if 75 // the plugin is disabled, but there is no harm in making sure core hasn't done something wrong. 76 if (!enrol_is_enabled('lti')) { 77 mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti')); 78 return; 79 } 80 81 $this->dataconnector = new data_connector(); 82 83 // Get all the enabled tools. 84 $tools = helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED, 'membersync' => 1, 85 'ltiversion' => 'LTI-1p0/LTI-2p0')); 86 foreach ($tools as $tool) { 87 mtrace("Starting - Member sync for published tool '$tool->id' for course '$tool->courseid'."); 88 89 // Variables to keep track of information to display later. 90 $usercount = 0; 91 $enrolcount = 0; 92 $unenrolcount = 0; 93 94 // Fetch consumer records mapped to this tool. 95 $consumers = $this->dataconnector->get_consumers_mapped_to_tool($tool->id); 96 97 // Perform processing for each consumer. 98 foreach ($consumers as $consumer) { 99 mtrace("Requesting membership service for the tool consumer '{$consumer->getRecordId()}'"); 100 101 // Get members through this tool consumer. 102 $members = $this->fetch_members_from_consumer($consumer); 103 104 // Check if we were able to fetch the members. 105 if ($members === false) { 106 mtrace("Skipping - Membership service request failed.\n"); 107 continue; 108 } 109 110 // Fetched members count. 111 $membercount = count($members); 112 $usercount += $membercount; 113 mtrace("$membercount members received.\n"); 114 115 // Process member information. 116 list($users, $enrolledcount) = $this->sync_member_information($tool, $consumer, $members); 117 $enrolcount += $enrolledcount; 118 119 // Now sync unenrolments for the consumer. 120 $unenrolcount += $this->sync_unenrol($tool, $consumer->getKey(), $users); 121 } 122 123 mtrace("Completed - Synced members for tool '$tool->id' in the course '$tool->courseid'. " . 124 "Processed $usercount users; enrolled $enrolcount members; unenrolled $unenrolcount members.\n"); 125 } 126 127 // Sync the user profile photos. 128 mtrace("Started - Syncing user profile images."); 129 $countsyncedimages = $this->sync_profile_images(); 130 mtrace("Completed - Synced $countsyncedimages profile images."); 131 } 132 133 /** 134 * Fetches the members that belong to a ToolConsumer. 135 * 136 * @param ToolConsumer $consumer 137 * @return bool|User[] 138 */ 139 protected function fetch_members_from_consumer(ToolConsumer $consumer) { 140 $dataconnector = $this->dataconnector; 141 142 // Get membership URL template from consumer profile data. 143 $defaultmembershipsurl = null; 144 if (isset($consumer->profile->service_offered)) { 145 $servicesoffered = $consumer->profile->service_offered; 146 foreach ($servicesoffered as $service) { 147 if (isset($service->{'@id'}) && strpos($service->{'@id'}, 'tcp:ToolProxyBindingMemberships') !== false && 148 isset($service->endpoint)) { 149 $defaultmembershipsurl = $service->endpoint; 150 if (isset($consumer->profile->product_instance->product_info->product_family->vendor->code)) { 151 $vendorcode = $consumer->profile->product_instance->product_info->product_family->vendor->code; 152 $defaultmembershipsurl = str_replace('{vendor_code}', $vendorcode, $defaultmembershipsurl); 153 } 154 $defaultmembershipsurl = str_replace('{product_code}', $consumer->getKey(), $defaultmembershipsurl); 155 break; 156 } 157 } 158 } 159 160 $members = false; 161 162 // Fetch the resource link linked to the consumer. 163 $resourcelink = $dataconnector->get_resourcelink_from_consumer($consumer); 164 if ($resourcelink !== null) { 165 // Try to perform a membership service request using this resource link. 166 $members = $this->do_resourcelink_membership_request($resourcelink); 167 } 168 169 // If membership service can't be performed through resource link, fallback through context memberships. 170 if ($members === false) { 171 // Fetch context records that are mapped to this ToolConsumer. 172 $contexts = $dataconnector->get_contexts_from_consumer($consumer); 173 174 // Perform membership service request for each of these contexts. 175 foreach ($contexts as $context) { 176 $contextmembership = $this->do_context_membership_request($context, $resourcelink, $defaultmembershipsurl); 177 if ($contextmembership) { 178 // Add $contextmembership contents to $members array. 179 if (is_array($members)) { 180 $members = array_merge($members, $contextmembership); 181 } else { 182 $members = $contextmembership; 183 } 184 } 185 } 186 } 187 188 return $members; 189 } 190 191 /** 192 * Method to determine whether to sync unenrolments or not. 193 * 194 * @param int $syncmode The tool's membersyncmode. 195 * @return bool 196 */ 197 protected function should_sync_unenrol($syncmode) { 198 return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_UNENROL_MISSING; 199 } 200 201 /** 202 * Method to determine whether to sync enrolments or not. 203 * 204 * @param int $syncmode The tool's membersyncmode. 205 * @return bool 206 */ 207 protected function should_sync_enrol($syncmode) { 208 return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_ENROL_NEW; 209 } 210 211 /** 212 * Performs synchronisation of member information and enrolments. 213 * 214 * @param stdClass $tool 215 * @param ToolConsumer $consumer 216 * @param User[] $members 217 * @return array An array of users from processed members and the number that were enrolled. 218 */ 219 protected function sync_member_information(stdClass $tool, ToolConsumer $consumer, $members) { 220 global $DB; 221 $users = []; 222 $enrolcount = 0; 223 224 // Process member information. 225 foreach ($members as $member) { 226 // Set the user data. 227 $user = new stdClass(); 228 $user->username = helper::create_username($consumer->getKey(), $member->ltiUserId); 229 $user->firstname = core_user::clean_field($member->firstname, 'firstname'); 230 $user->lastname = core_user::clean_field($member->lastname, 'lastname'); 231 $user->email = core_user::clean_field($member->email, 'email'); 232 233 // Get the user data from the LTI consumer. 234 $user = helper::assign_user_tool_data($tool, $user); 235 236 $dbuser = core_user::get_user_by_username($user->username, 'id'); 237 if ($dbuser) { 238 // If email is empty remove it, so we don't update the user with an empty email. 239 if (empty($user->email)) { 240 unset($user->email); 241 } 242 243 $user->id = $dbuser->id; 244 user_update_user($user); 245 246 // Add the information to the necessary arrays. 247 $users[$user->id] = $user; 248 $this->userphotos[$user->id] = $member->image; 249 } else { 250 if ($this->should_sync_enrol($tool->membersyncmode)) { 251 // If the email was stripped/not set then fill it with a default one. This 252 // stops the user from being redirected to edit their profile page. 253 if (empty($user->email)) { 254 $user->email = $user->username . "@example.com"; 255 } 256 257 $user->auth = 'lti'; 258 $user->id = user_create_user($user); 259 260 // Add the information to the necessary arrays. 261 $users[$user->id] = $user; 262 $this->userphotos[$user->id] = $member->image; 263 } 264 } 265 266 // Sync enrolments. 267 if ($this->should_sync_enrol($tool->membersyncmode)) { 268 // Enrol the user in the course. 269 if (helper::enrol_user($tool, $user->id) === helper::ENROLMENT_SUCCESSFUL) { 270 // Increment enrol count. 271 $enrolcount++; 272 } 273 274 // Check if this user has already been registered in the enrol_lti_users table. 275 if (!$DB->record_exists('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) { 276 // Create an initial enrol_lti_user record that we can use later when syncing grades and members. 277 $userlog = new stdClass(); 278 $userlog->userid = $user->id; 279 $userlog->toolid = $tool->id; 280 $userlog->consumerkey = $consumer->getKey(); 281 282 $DB->insert_record('enrol_lti_users', $userlog); 283 } 284 } 285 } 286 287 return [$users, $enrolcount]; 288 } 289 290 /** 291 * Performs unenrolment of users that are no longer enrolled in the consumer side. 292 * 293 * @param stdClass $tool The tool record object. 294 * @param string $consumerkey ensure we only unenrol users from this tool consumer. 295 * @param array $currentusers The list of current users. 296 * @return int The number of users that have been unenrolled. 297 */ 298 protected function sync_unenrol(stdClass $tool, string $consumerkey, array $currentusers) { 299 global $DB; 300 301 $ltiplugin = enrol_get_plugin('lti'); 302 303 if (!$this->should_sync_unenrol($tool->membersyncmode)) { 304 return 0; 305 } 306 307 if (empty($currentusers)) { 308 return 0; 309 } 310 311 $unenrolcount = 0; 312 313 $select = "toolid = :toolid AND " . $DB->sql_compare_text('consumerkey', 255) . " = :consumerkey"; 314 $ltiusersrs = $DB->get_recordset_select('enrol_lti_users', $select, ['toolid' => $tool->id, 'consumerkey' => $consumerkey], 315 'lastaccess DESC', 'userid'); 316 // Go through the users and check if any were never listed, if so, remove them. 317 foreach ($ltiusersrs as $ltiuser) { 318 if (!array_key_exists($ltiuser->userid, $currentusers)) { 319 $instance = new stdClass(); 320 $instance->id = $tool->enrolid; 321 $instance->courseid = $tool->courseid; 322 $instance->enrol = 'lti'; 323 $ltiplugin->unenrol_user($instance, $ltiuser->userid); 324 // Increment unenrol count. 325 $unenrolcount++; 326 } 327 } 328 $ltiusersrs->close(); 329 330 return $unenrolcount; 331 } 332 333 /** 334 * Performs synchronisation of user profile images. 335 */ 336 protected function sync_profile_images() { 337 $counter = 0; 338 foreach ($this->userphotos as $userid => $url) { 339 if ($url) { 340 $result = helper::update_user_profile_image($userid, $url); 341 if ($result === helper::PROFILE_IMAGE_UPDATE_SUCCESSFUL) { 342 $counter++; 343 mtrace("Profile image succesfully downloaded and created for user '$userid' from $url."); 344 } else { 345 mtrace($result); 346 } 347 } 348 } 349 return $counter; 350 } 351 352 /** 353 * Performs membership service request using an LTI Context object. 354 * 355 * If the context has a 'custom_context_memberships_url' setting, we use this to perform the membership service request. 356 * Otherwise, if a context is associated with resource link, we try first to get the members using the 357 * ResourceLink::doMembershipsService() method. 358 * If we're still unable to fetch members from the resource link, we try to build a memberships URL from the memberships URL 359 * endpoint template that is defined in the ToolConsumer profile and substitute the parameters accordingly. 360 * 361 * @param Context $context The context object. 362 * @param ResourceLink $resourcelink The resource link object. 363 * @param string $membershipsurltemplate The memberships endpoint URL template. 364 * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise. 365 */ 366 protected function do_context_membership_request(Context $context, ResourceLink $resourcelink = null, 367 $membershipsurltemplate = '') { 368 $dataconnector = $this->dataconnector; 369 370 // Flag to indicate whether to save the context later. 371 $contextupdated = false; 372 373 // If membership URL is not set, try to generate using the default membership URL from the consumer profile. 374 if (!$context->hasMembershipService()) { 375 if (empty($membershipsurltemplate)) { 376 mtrace("Skipping - No membership service available.\n"); 377 return false; 378 } 379 380 if ($resourcelink === null) { 381 $resourcelink = $dataconnector->get_resourcelink_from_context($context); 382 } 383 384 if ($resourcelink !== null) { 385 // Try to perform a membership service request using this resource link. 386 $resourcelinkmembers = $this->do_resourcelink_membership_request($resourcelink); 387 if ($resourcelinkmembers) { 388 // If we're able to fetch members using this resource link, return these. 389 return $resourcelinkmembers; 390 } 391 } 392 393 // If fetching memberships through resource link failed and we don't have a memberships URL, build one from template. 394 mtrace("'custom_context_memberships_url' not set. Fetching default template: $membershipsurltemplate"); 395 $membershipsurl = $membershipsurltemplate; 396 397 // Check if we need to fetch tool code. 398 $needstoolcode = strpos($membershipsurl, '{tool_code}') !== false; 399 if ($needstoolcode) { 400 $toolcode = false; 401 402 // Fetch tool code from the resource link data. 403 $lisresultsourcedidjson = $resourcelink->getSetting('lis_result_sourcedid'); 404 if ($lisresultsourcedidjson) { 405 $lisresultsourcedid = json_decode($lisresultsourcedidjson); 406 if (isset($lisresultsourcedid->data->typeid)) { 407 $toolcode = $lisresultsourcedid->data->typeid; 408 } 409 } 410 411 if ($toolcode) { 412 // Substitute fetched tool code value. 413 $membershipsurl = str_replace('{tool_code}', $toolcode, $membershipsurl); 414 } else { 415 // We're unable to determine the tool code. End this processing. 416 return false; 417 } 418 } 419 420 // Get context_id parameter and substitute, if applicable. 421 $membershipsurl = str_replace('{context_id}', $context->getId(), $membershipsurl); 422 423 // Get context_type and substitute, if applicable. 424 if (strpos($membershipsurl, '{context_type}') !== false) { 425 $contexttype = $context->type !== null ? $context->type : 'CourseSection'; 426 $membershipsurl = str_replace('{context_type}', $contexttype, $membershipsurl); 427 } 428 429 // Save this URL for the context's custom_context_memberships_url setting. 430 $context->setSetting('custom_context_memberships_url', $membershipsurl); 431 $contextupdated = true; 432 } 433 434 // Perform membership service request. 435 $url = $context->getSetting('custom_context_memberships_url'); 436 mtrace("Performing membership service request from context with URL {$url}."); 437 $members = $context->getMembership(); 438 439 // Save the context if membership request succeeded and if it has been updated. 440 if ($members && $contextupdated) { 441 $context->save(); 442 } 443 444 return $members; 445 } 446 447 /** 448 * Performs membership service request using ResourceLink::doMembershipsService() method. 449 * 450 * @param ResourceLink $resourcelink 451 * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise. 452 */ 453 protected function do_resourcelink_membership_request(ResourceLink $resourcelink) { 454 $members = false; 455 $membershipsurl = $resourcelink->getSetting('ext_ims_lis_memberships_url'); 456 $membershipsid = $resourcelink->getSetting('ext_ims_lis_memberships_id'); 457 if ($membershipsurl && $membershipsid) { 458 mtrace("Performing membership service request from resource link with membership URL: " . $membershipsurl); 459 $members = $resourcelink->doMembershipsService(true); 460 } 461 return $members; 462 } 463 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body