See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 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 * Extends the IMS Tool provider library for the LTI enrolment. 19 * 20 * @package enrol_lti 21 * @copyright 2016 John Okely <john@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace enrol_lti; 26 27 defined('MOODLE_INTERNAL') || die; 28 29 use context; 30 use core\notification; 31 use core_user; 32 use enrol_lti\output\registration; 33 use html_writer; 34 use IMSGlobal\LTI\Profile\Item; 35 use IMSGlobal\LTI\Profile\Message; 36 use IMSGlobal\LTI\Profile\ResourceHandler; 37 use IMSGlobal\LTI\Profile\ServiceDefinition; 38 use IMSGlobal\LTI\ToolProvider\ToolProvider; 39 use moodle_exception; 40 use moodle_url; 41 use stdClass; 42 43 require_once($CFG->dirroot . '/user/lib.php'); 44 45 /** 46 * Extends the IMS Tool provider library for the LTI enrolment. 47 * 48 * @package enrol_lti 49 * @copyright 2016 John Okely <john@moodle.com> 50 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 51 */ 52 class tool_provider extends ToolProvider { 53 54 /** 55 * @var stdClass $tool The object representing the enrol instance providing this LTI tool 56 */ 57 protected $tool; 58 59 /** 60 * Remove $this->baseUrl (wwwroot) from a given url string and return it. 61 * 62 * @param string $url The url from which to remove the base url 63 * @return string|null A string of the relative path to the url, or null if it couldn't be determined. 64 */ 65 protected function strip_base_url($url) { 66 if (substr($url, 0, strlen($this->baseUrl)) == $this->baseUrl) { 67 return substr($url, strlen($this->baseUrl)); 68 } 69 return null; 70 } 71 72 /** 73 * Create a new instance of tool_provider to handle all the LTI tool provider interactions. 74 * 75 * @param int $toolid The id of the tool to be provided. 76 */ 77 public function __construct($toolid) { 78 global $CFG, $SITE; 79 80 $token = helper::generate_proxy_token($toolid); 81 82 $tool = helper::get_lti_tool($toolid); 83 $this->tool = $tool; 84 85 $dataconnector = new data_connector(); 86 parent::__construct($dataconnector); 87 88 // Override debugMode and set to the configured value. 89 $this->debugMode = $CFG->debugdeveloper; 90 91 $this->baseUrl = $CFG->wwwroot; 92 $toolpath = helper::get_launch_url($toolid); 93 $toolpath = $this->strip_base_url($toolpath); 94 95 $vendorid = $SITE->shortname; 96 $vendorname = $SITE->fullname; 97 $vendordescription = trim(html_to_text($SITE->summary)); 98 $this->vendor = new Item($vendorid, $vendorname, $vendordescription, $CFG->wwwroot); 99 100 $name = helper::get_name($tool); 101 $description = helper::get_description($tool); 102 $icon = helper::get_icon($tool)->out(); 103 $icon = $this->strip_base_url($icon); 104 105 $this->product = new Item( 106 $token, 107 $name, 108 $description, 109 helper::get_proxy_url($tool), 110 '1.0' 111 ); 112 113 $requiredmessages = [ 114 new Message( 115 'basic-lti-launch-request', 116 $toolpath, 117 [ 118 'Context.id', 119 'CourseSection.title', 120 'CourseSection.label', 121 'CourseSection.sourcedId', 122 'CourseSection.longDescription', 123 'CourseSection.timeFrame.begin', 124 'ResourceLink.id', 125 'ResourceLink.title', 126 'ResourceLink.description', 127 'User.id', 128 'User.username', 129 'Person.name.full', 130 'Person.name.given', 131 'Person.name.family', 132 'Person.email.primary', 133 'Person.sourcedId', 134 'Person.name.middle', 135 'Person.address.street1', 136 'Person.address.locality', 137 'Person.address.country', 138 'Person.address.timezone', 139 'Person.phone.primary', 140 'Person.phone.mobile', 141 'Person.webaddress', 142 'Membership.role', 143 'Result.sourcedId', 144 'Result.autocreate' 145 ] 146 ) 147 ]; 148 $optionalmessages = [ 149 ]; 150 151 $this->resourceHandlers[] = new ResourceHandler( 152 new Item( 153 $token, 154 helper::get_name($tool), 155 $description 156 ), 157 $icon, 158 $requiredmessages, 159 $optionalmessages 160 ); 161 162 $this->requiredServices[] = new ServiceDefinition(['application/vnd.ims.lti.v2.toolproxy+json'], ['POST']); 163 $this->requiredServices[] = new ServiceDefinition(['application/vnd.ims.lis.v2.membershipcontainer+json'], ['GET']); 164 } 165 166 /** 167 * Override onError for custom error handling. 168 * @return void 169 */ 170 protected function onError() { 171 global $OUTPUT; 172 173 $message = $this->message; 174 if ($this->debugMode && !empty($this->reason)) { 175 $message = $this->reason; 176 } 177 178 // Display the error message from the provider's side if the consumer has not specified a URL to pass the error to. 179 if (empty($this->returnUrl)) { 180 $this->errorOutput = $OUTPUT->notification(get_string('failedrequest', 'enrol_lti', ['reason' => $message]), 'error'); 181 } 182 } 183 184 /** 185 * Override onLaunch with tool logic. 186 * @return void 187 */ 188 protected function onLaunch() { 189 global $DB, $SESSION, $CFG; 190 191 // Check for valid consumer. 192 if (empty($this->consumer) || $this->dataConnector->loadToolConsumer($this->consumer) === false) { 193 $this->ok = false; 194 $this->message = get_string('invalidtoolconsumer', 'enrol_lti'); 195 return; 196 } 197 198 $url = helper::get_launch_url($this->tool->id); 199 // If a tool proxy has been stored for the current consumer trying to access a tool, 200 // check that the tool is being launched from the correct url. 201 $correctlaunchurl = false; 202 if (!empty($this->consumer->toolProxy)) { 203 $proxy = json_decode($this->consumer->toolProxy); 204 $handlers = $proxy->tool_profile->resource_handler; 205 foreach ($handlers as $handler) { 206 foreach ($handler->message as $message) { 207 $handlerurl = new moodle_url($message->path); 208 $fullpath = $handlerurl->out(false); 209 if ($message->message_type == "basic-lti-launch-request" && $fullpath == $url) { 210 $correctlaunchurl = true; 211 break 2; 212 } 213 } 214 } 215 } else if ($this->tool->secret == $this->consumer->secret) { 216 // Test if the LTI1 secret for this tool is being used. Then we know the correct tool is being launched. 217 $correctlaunchurl = true; 218 } 219 if (!$correctlaunchurl) { 220 $this->ok = false; 221 $this->message = get_string('invalidrequest', 'enrol_lti'); 222 return; 223 } 224 225 // Before we do anything check that the context is valid. 226 $tool = $this->tool; 227 $context = context::instance_by_id($tool->contextid); 228 229 // Set the user data. 230 $user = new stdClass(); 231 $user->username = helper::create_username($this->consumer->getKey(), $this->user->ltiUserId); 232 if (!empty($this->user->firstname)) { 233 $user->firstname = $this->user->firstname; 234 } else { 235 $user->firstname = $this->user->getRecordId(); 236 } 237 if (!empty($this->user->lastname)) { 238 $user->lastname = $this->user->lastname; 239 } else { 240 $user->lastname = $this->tool->contextid; 241 } 242 243 $user->email = core_user::clean_field($this->user->email, 'email'); 244 245 // Get the user data from the LTI consumer. 246 $user = helper::assign_user_tool_data($tool, $user); 247 248 // Check if the user exists. 249 if (!$dbuser = $DB->get_record('user', ['username' => $user->username, 'deleted' => 0])) { 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 // Get the updated user record. 260 $user = $DB->get_record('user', ['id' => $user->id]); 261 } else { 262 if (helper::user_match($user, $dbuser)) { 263 $user = $dbuser; 264 } else { 265 // If email is empty remove it, so we don't update the user with an empty email. 266 if (empty($user->email)) { 267 unset($user->email); 268 } 269 270 $user->id = $dbuser->id; 271 \user_update_user($user); 272 273 // Get the updated user record. 274 $user = $DB->get_record('user', ['id' => $user->id]); 275 } 276 } 277 278 // Update user image. 279 if (isset($this->user) && isset($this->user->image) && !empty($this->user->image)) { 280 $image = $this->user->image; 281 } else { 282 // Use custom_user_image parameter as a fallback. 283 $image = $this->resourceLink->getSetting('custom_user_image'); 284 } 285 286 // Check if there is an image to process. 287 if ($image) { 288 helper::update_user_profile_image($user->id, $image); 289 } 290 291 // Check if we need to force the page layout to embedded. 292 $isforceembed = $this->resourceLink->getSetting('custom_force_embed') == 1; 293 294 // Check if we are an instructor. 295 $isinstructor = $this->user->isStaff() || $this->user->isAdmin(); 296 297 if ($context->contextlevel == CONTEXT_COURSE) { 298 $courseid = $context->instanceid; 299 $urltogo = new moodle_url('/course/view.php', ['id' => $courseid]); 300 301 } else if ($context->contextlevel == CONTEXT_MODULE) { 302 $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); 303 $urltogo = new moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]); 304 305 // If we are a student in the course module context we do not want to display blocks. 306 if (!$isforceembed && !$isinstructor) { 307 $isforceembed = true; 308 } 309 } else { 310 throw new \moodle_exception('invalidcontext'); 311 exit(); 312 } 313 314 // Force page layout to embedded if necessary. 315 if ($isforceembed) { 316 $SESSION->forcepagelayout = 'embedded'; 317 } else { 318 // May still be set from previous session, so unset it. 319 unset($SESSION->forcepagelayout); 320 } 321 322 // Enrol the user in the course with no role. 323 $result = helper::enrol_user($tool, $user->id); 324 325 // Display an error, if there is one. 326 if ($result !== helper::ENROLMENT_SUCCESSFUL) { 327 throw new \moodle_exception($result, 'enrol_lti'); 328 exit(); 329 } 330 331 // Give the user the role in the given context. 332 $roleid = $isinstructor ? $tool->roleinstructor : $tool->rolelearner; 333 role_assign($roleid, $user->id, $tool->contextid); 334 335 // Login user. 336 $sourceid = $this->user->ltiResultSourcedId; 337 $serviceurl = $this->resourceLink->getSetting('lis_outcome_service_url'); 338 339 // Check if we have recorded this user before. 340 if ($userlog = $DB->get_record('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) { 341 if ($userlog->sourceid != $sourceid) { 342 $userlog->sourceid = $sourceid; 343 } 344 if ($userlog->serviceurl != $serviceurl) { 345 $userlog->serviceurl = $serviceurl; 346 } 347 if (empty($userlog->consumersecret)) { 348 $userlog->consumersecret = $this->consumer->secret; 349 } 350 $userlog->lastaccess = time(); 351 $DB->update_record('enrol_lti_users', $userlog); 352 } else { 353 // Add the user details so we can use it later when syncing grades and members. 354 $userlog = new stdClass(); 355 $userlog->userid = $user->id; 356 $userlog->toolid = $tool->id; 357 $userlog->serviceurl = $serviceurl; 358 $userlog->sourceid = $sourceid; 359 $userlog->consumerkey = $this->consumer->getKey(); 360 $userlog->consumersecret = $this->consumer->secret; 361 $userlog->lastgrade = 0; 362 $userlog->lastaccess = time(); 363 $userlog->timecreated = time(); 364 $userlog->membershipsurl = $this->resourceLink->getSetting('ext_ims_lis_memberships_url'); 365 $userlog->membershipsid = $this->resourceLink->getSetting('ext_ims_lis_memberships_id'); 366 367 $DB->insert_record('enrol_lti_users', $userlog); 368 } 369 370 // Finalise the user log in. 371 complete_user_login($user); 372 373 // Everything's good. Set appropriate OK flag and message values. 374 $this->ok = true; 375 $this->message = get_string('success'); 376 377 if (empty($CFG->allowframembedding)) { 378 // Provide an alternative link. 379 $stropentool = get_string('opentool', 'enrol_lti'); 380 echo html_writer::tag('p', get_string('frameembeddingnotenabled', 'enrol_lti')); 381 echo html_writer::link($urltogo, $stropentool, ['target' => '_blank']); 382 } else { 383 // All done, redirect the user to where they want to go. 384 redirect($urltogo); 385 } 386 } 387 388 /** 389 * Override onRegister with registration code. 390 */ 391 protected function onRegister() { 392 global $PAGE; 393 394 if (empty($this->consumer)) { 395 $this->ok = false; 396 $this->message = get_string('invalidtoolconsumer', 'enrol_lti'); 397 return; 398 } 399 400 if (empty($this->returnUrl)) { 401 $this->ok = false; 402 $this->message = get_string('returnurlnotset', 'enrol_lti'); 403 return; 404 } 405 406 if ($this->doToolProxyService()) { 407 // Map tool consumer and published tool, if necessary. 408 $this->map_tool_to_consumer(); 409 410 // Indicate successful processing in message. 411 $this->message = get_string('successfulregistration', 'enrol_lti'); 412 413 // Prepare response. 414 $returnurl = new moodle_url($this->returnUrl); 415 $returnurl->param('lti_msg', get_string("successfulregistration", "enrol_lti")); 416 $returnurl->param('status', 'success'); 417 $guid = $this->consumer->getKey(); 418 $returnurl->param('tool_proxy_guid', $guid); 419 420 $returnurlout = $returnurl->out(false); 421 422 $registration = new registration($returnurlout); 423 $output = $PAGE->get_renderer('enrol_lti'); 424 echo $output->render($registration); 425 426 } else { 427 // Tell the consumer that the registration failed. 428 $this->ok = false; 429 $this->message = get_string('couldnotestablishproxy', 'enrol_lti'); 430 } 431 } 432 433 /** 434 * Performs mapping of the tool consumer to a published tool. 435 * 436 * @throws moodle_exception 437 */ 438 public function map_tool_to_consumer() { 439 global $DB; 440 441 if (empty($this->consumer)) { 442 throw new moodle_exception('invalidtoolconsumer', 'enrol_lti'); 443 } 444 445 // Map the consumer to the tool. 446 $mappingparams = [ 447 'toolid' => $this->tool->id, 448 'consumerid' => $this->consumer->getRecordId() 449 ]; 450 $mappingexists = $DB->record_exists('enrol_lti_tool_consumer_map', $mappingparams); 451 if (!$mappingexists) { 452 $DB->insert_record('enrol_lti_tool_consumer_map', (object) $mappingparams); 453 } 454 } 455 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body