Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 foreach ($tools as $tool) { 86 mtrace("Starting - Member sync for published tool '$tool->id' for course '$tool->courseid'."); 87 88 // Variables to keep track of information to display later. 89 $usercount = 0; 90 $enrolcount = 0; 91 $unenrolcount = 0; 92 93 // Fetch consumer records mapped to this tool. 94 $consumers = $this->dataconnector->get_consumers_mapped_to_tool($tool->id); 95 96 // Perform processing for each consumer. 97 foreach ($consumers as $consumer) { 98 mtrace("Requesting membership service for the tool consumer '{$consumer->getRecordId()}'"); 99 100 // Get members through this tool consumer. 101 $members = $this->fetch_members_from_consumer($consumer); 102 103 // Check if we were able to fetch the members. 104 if ($members === false) { 105 mtrace("Skipping - Membership service request failed.\n"); 106 continue; 107 } 108 109 // Fetched members count. 110 $membercount = count($members); 111 $usercount += $membercount; 112 mtrace("$membercount members received.\n"); 113 114 // Process member information. 115 list($users, $enrolledcount) = $this->sync_member_information($tool, $consumer, $members); 116 $enrolcount += $enrolledcount; 117 118 // Now sync unenrolments for the consumer. 119 $unenrolcount += $this->sync_unenrol($tool, $consumer->getKey(), $users); 120 } 121 122 mtrace("Completed - Synced members for tool '$tool->id' in the course '$tool->courseid'. " . 123 "Processed $usercount users; enrolled $enrolcount members; unenrolled $unenrolcount members.\n"); 124 } 125 126 // Sync the user profile photos. 127 mtrace("Started - Syncing user profile images."); 128 $countsyncedimages = $this->sync_profile_images(); 129 mtrace("Completed - Synced $countsyncedimages profile images."); 130 } 131 132 /** 133 * Fetches the members that belong to a ToolConsumer. 134 * 135 * @param ToolConsumer $consumer 136 * @return bool|User[] 137 */ 138 protected function fetch_members_from_consumer(ToolConsumer $consumer) { 139 $dataconnector = $this->dataconnector; 140 141 // Get membership URL template from consumer profile data. 142 $defaultmembershipsurl = null; 143 if (isset($consumer->profile->service_offered)) { 144 $servicesoffered = $consumer->profile->service_offered; 145 foreach ($servicesoffered as $service) { 146 if (isset($service->{'@id'}) && strpos($service->{'@id'}, 'tcp:ToolProxyBindingMemberships') !== false && 147 isset($service->endpoint)) { 148 $defaultmembershipsurl = $service->endpoint; 149 if (isset($consumer->profile->product_instance->product_info->product_family->vendor->code)) { 150 $vendorcode = $consumer->profile->product_instance->product_info->product_family->vendor->code; 151 $defaultmembershipsurl = str_replace('{vendor_code}', $vendorcode, $defaultmembershipsurl); 152 } 153 $defaultmembershipsurl = str_replace('{product_code}', $consumer->getKey(), $defaultmembershipsurl); 154 break; 155 } 156 } 157 } 158 159 $members = false; 160 161 // Fetch the resource link linked to the consumer. 162 $resourcelink = $dataconnector->get_resourcelink_from_consumer($consumer); 163 if ($resourcelink !== null) { 164 // Try to perform a membership service request using this resource link. 165 $members = $this->do_resourcelink_membership_request($resourcelink); 166 } 167 168 // If membership service can't be performed through resource link, fallback through context memberships. 169 if ($members === false) { 170 // Fetch context records that are mapped to this ToolConsumer. 171 $contexts = $dataconnector->get_contexts_from_consumer($consumer); 172 173 // Perform membership service request for each of these contexts. 174 foreach ($contexts as $context) { 175 $contextmembership = $this->do_context_membership_request($context, $resourcelink, $defaultmembershipsurl); 176 if ($contextmembership) { 177 // Add $contextmembership contents to $members array. 178 if (is_array($members)) { 179 $members = array_merge($members, $contextmembership); 180 } else { 181 $members = $contextmembership; 182 } 183 } 184 } 185 } 186 187 return $members; 188 } 189 190 /** 191 * Method to determine whether to sync unenrolments or not. 192 * 193 * @param int $syncmode The tool's membersyncmode. 194 * @return bool 195 */ 196 protected function should_sync_unenrol($syncmode) { 197 return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_UNENROL_MISSING; 198 } 199 200 /** 201 * Method to determine whether to sync enrolments or not. 202 * 203 * @param int $syncmode The tool's membersyncmode. 204 * @return bool 205 */ 206 protected function should_sync_enrol($syncmode) { 207 return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_ENROL_NEW; 208 } 209 210 /** 211 * Performs synchronisation of member information and enrolments. 212 * 213 * @param stdClass $tool 214 * @param ToolConsumer $consumer 215 * @param User[] $members 216 * @return array An array of users from processed members and the number that were enrolled. 217 */ 218 protected function sync_member_information(stdClass $tool, ToolConsumer $consumer, $members) { 219 global $DB; 220 $users = []; 221 $enrolcount = 0; 222 223 // Process member information. 224 foreach ($members as $member) { 225 // Set the user data. 226 $user = new stdClass(); 227 $user->username = helper::create_username($consumer->getKey(), $member->ltiUserId); 228 $user->firstname = core_user::clean_field($member->firstname, 'firstname'); 229 $user->lastname = core_user::clean_field($member->lastname, 'lastname'); 230 $user->email = core_user::clean_field($member->email, 'email'); 231 232 // Get the user data from the LTI consumer. 233 $user = helper::assign_user_tool_data($tool, $user); 234 235 $dbuser = core_user::get_user_by_username($user->username, 'id'); 236 if ($dbuser) { 237 // If email is empty remove it, so we don't update the user with an empty email. 238 if (empty($user->email)) { 239 unset($user->email); 240 } 241 242 $user->id = $dbuser->id; 243 user_update_user($user); 244 245 // Add the information to the necessary arrays. 246 $users[$user->id] = $user; 247 $this->userphotos[$user->id] = $member->image; 248 } else { 249 if ($this->should_sync_enrol($tool->membersyncmode)) { 250 // If the email was stripped/not set then fill it with a default one. This 251 // stops the user from being redirected to edit their profile page. 252 if (empty($user->email)) { 253 $user->email = $user->username . "@example.com"; 254 } 255 256 $user->auth = 'lti'; 257 $user->id = user_create_user($user); 258 259 // Add the information to the necessary arrays. 260 $users[$user->id] = $user; 261 $this->userphotos[$user->id] = $member->image; 262 } 263 } 264 265 // Sync enrolments. 266 if ($this->should_sync_enrol($tool->membersyncmode)) { 267 // Enrol the user in the course. 268 if (helper::enrol_user($tool, $user->id) === helper::ENROLMENT_SUCCESSFUL) { 269 // Increment enrol count. 270 $enrolcount++; 271 } 272 273 // Check if this user has already been registered in the enrol_lti_users table. 274 if (!$DB->record_exists('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) { 275 // Create an initial enrol_lti_user record that we can use later when syncing grades and members. 276 $userlog = new stdClass(); 277 $userlog->userid = $user->id; 278 $userlog->toolid = $tool->id; 279 $userlog->consumerkey = $consumer->getKey(); 280 281 $DB->insert_record('enrol_lti_users', $userlog); 282 } 283 } 284 } 285 286 return [$users, $enrolcount]; 287 } 288 289 /** 290 * Performs unenrolment of users that are no longer enrolled in the consumer side. 291 * 292 * @param stdClass $tool The tool record object. 293 * @param string $consumerkey ensure we only unenrol users from this tool consumer. 294 * @param array $currentusers The list of current users. 295 * @return int The number of users that have been unenrolled. 296 */ 297 protected function sync_unenrol(stdClass $tool, string $consumerkey, array $currentusers) { 298 global $DB; 299 300 $ltiplugin = enrol_get_plugin('lti'); 301 302 if (!$this->should_sync_unenrol($tool->membersyncmode)) { 303 return 0; 304 } 305 306 if (empty($currentusers)) { 307 return 0; 308 } 309 310 $unenrolcount = 0; 311 312 $select = "toolid = :toolid AND " . $DB->sql_compare_text('consumerkey', 255) . " = :consumerkey"; 313 $ltiusersrs = $DB->get_recordset_select('enrol_lti_users', $select, ['toolid' => $tool->id, 'consumerkey' => $consumerkey], 314 'lastaccess DESC', 'userid'); 315 // Go through the users and check if any were never listed, if so, remove them. 316 foreach ($ltiusersrs as $ltiuser) { 317 if (!array_key_exists($ltiuser->userid, $currentusers)) { 318 $instance = new stdClass(); 319 $instance->id = $tool->enrolid; 320 $instance->courseid = $tool->courseid; 321 $instance->enrol = 'lti'; 322 $ltiplugin->unenrol_user($instance, $ltiuser->userid); 323 // Increment unenrol count. 324 $unenrolcount++; 325 } 326 } 327 $ltiusersrs->close(); 328 329 return $unenrolcount; 330 } 331 332 /** 333 * Performs synchronisation of user profile images. 334 */ 335 protected function sync_profile_images() { 336 $counter = 0; 337 foreach ($this->userphotos as $userid => $url) { 338 if ($url) { 339 $result = helper::update_user_profile_image($userid, $url); 340 if ($result === helper::PROFILE_IMAGE_UPDATE_SUCCESSFUL) { 341 $counter++; 342 mtrace("Profile image succesfully downloaded and created for user '$userid' from $url."); 343 } else { 344 mtrace($result); 345 } 346 } 347 } 348 return $counter; 349 } 350 351 /** 352 * Performs membership service request using an LTI Context object. 353 * 354 * If the context has a 'custom_context_memberships_url' setting, we use this to perform the membership service request. 355 * Otherwise, if a context is associated with resource link, we try first to get the members using the 356 * ResourceLink::doMembershipsService() method. 357 * If we're still unable to fetch members from the resource link, we try to build a memberships URL from the memberships URL 358 * endpoint template that is defined in the ToolConsumer profile and substitute the parameters accordingly. 359 * 360 * @param Context $context The context object. 361 * @param ResourceLink $resourcelink The resource link object. 362 * @param string $membershipsurltemplate The memberships endpoint URL template. 363 * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise. 364 */ 365 protected function do_context_membership_request(Context $context, ResourceLink $resourcelink = null, 366 $membershipsurltemplate = '') { 367 $dataconnector = $this->dataconnector; 368 369 // Flag to indicate whether to save the context later. 370 $contextupdated = false; 371 372 // If membership URL is not set, try to generate using the default membership URL from the consumer profile. 373 if (!$context->hasMembershipService()) { 374 if (empty($membershipsurltemplate)) { 375 mtrace("Skipping - No membership service available.\n"); 376 return false; 377 } 378 379 if ($resourcelink === null) { 380 $resourcelink = $dataconnector->get_resourcelink_from_context($context); 381 } 382 383 if ($resourcelink !== null) { 384 // Try to perform a membership service request using this resource link. 385 $resourcelinkmembers = $this->do_resourcelink_membership_request($resourcelink); 386 if ($resourcelinkmembers) { 387 // If we're able to fetch members using this resource link, return these. 388 return $resourcelinkmembers; 389 } 390 } 391 392 // If fetching memberships through resource link failed and we don't have a memberships URL, build one from template. 393 mtrace("'custom_context_memberships_url' not set. Fetching default template: $membershipsurltemplate"); 394 $membershipsurl = $membershipsurltemplate; 395 396 // Check if we need to fetch tool code. 397 $needstoolcode = strpos($membershipsurl, '{tool_code}') !== false; 398 if ($needstoolcode) { 399 $toolcode = false; 400 401 // Fetch tool code from the resource link data. 402 $lisresultsourcedidjson = $resourcelink->getSetting('lis_result_sourcedid'); 403 if ($lisresultsourcedidjson) { 404 $lisresultsourcedid = json_decode($lisresultsourcedidjson); 405 if (isset($lisresultsourcedid->data->typeid)) { 406 $toolcode = $lisresultsourcedid->data->typeid; 407 } 408 } 409 410 if ($toolcode) { 411 // Substitute fetched tool code value. 412 $membershipsurl = str_replace('{tool_code}', $toolcode, $membershipsurl); 413 } else { 414 // We're unable to determine the tool code. End this processing. 415 return false; 416 } 417 } 418 419 // Get context_id parameter and substitute, if applicable. 420 $membershipsurl = str_replace('{context_id}', $context->getId(), $membershipsurl); 421 422 // Get context_type and substitute, if applicable. 423 if (strpos($membershipsurl, '{context_type}') !== false) { 424 $contexttype = $context->type !== null ? $context->type : 'CourseSection'; 425 $membershipsurl = str_replace('{context_type}', $contexttype, $membershipsurl); 426 } 427 428 // Save this URL for the context's custom_context_memberships_url setting. 429 $context->setSetting('custom_context_memberships_url', $membershipsurl); 430 $contextupdated = true; 431 } 432 433 // Perform membership service request. 434 $url = $context->getSetting('custom_context_memberships_url'); 435 mtrace("Performing membership service request from context with URL {$url}."); 436 $members = $context->getMembership(); 437 438 // Save the context if membership request succeeded and if it has been updated. 439 if ($members && $contextupdated) { 440 $context->save(); 441 } 442 443 return $members; 444 } 445 446 /** 447 * Performs membership service request using ResourceLink::doMembershipsService() method. 448 * 449 * @param ResourceLink $resourcelink 450 * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise. 451 */ 452 protected function do_resourcelink_membership_request(ResourceLink $resourcelink) { 453 $members = false; 454 $membershipsurl = $resourcelink->getSetting('ext_ims_lis_memberships_url'); 455 $membershipsid = $resourcelink->getSetting('ext_ims_lis_memberships_id'); 456 if ($membershipsurl && $membershipsid) { 457 mtrace("Performing membership service request from resource link with membership URL: " . $membershipsurl); 458 $members = $resourcelink->doMembershipsService(true); 459 } 460 return $members; 461 } 462 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body