2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Extends the IMS Tool provider library for the LTI enrolment.
21 * @copyright 2016 John Okely <john@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') ||
die;
30 use core\notification
;
32 use enrol_lti\output\registration
;
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
;
43 require_once($CFG->dirroot
. '/user/lib.php');
46 * Extends the IMS Tool provider library for the LTI enrolment.
49 * @copyright 2016 John Okely <john@moodle.com>
50 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
52 class tool_provider
extends ToolProvider
{
55 * @var stdClass $tool The object representing the enrol instance providing this LTI tool
60 * Remove $this->baseUrl (wwwroot) from a given url string and return it.
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.
65 protected function strip_base_url($url) {
66 if (substr($url, 0, strlen($this->baseUrl
)) == $this->baseUrl
) {
67 return substr($url, strlen($this->baseUrl
));
73 * Create a new instance of tool_provider to handle all the LTI tool provider interactions.
75 * @param int $toolid The id of the tool to be provided.
77 public function __construct($toolid) {
80 $token = helper
::generate_proxy_token($toolid);
82 $tool = helper
::get_lti_tool($toolid);
85 $dataconnector = new data_connector();
86 parent
::__construct($dataconnector);
88 // Override debugMode and set to the configured value.
89 $this->debugMode
= $CFG->debugdeveloper
;
91 $this->baseUrl
= $CFG->wwwroot
;
92 $toolpath = helper
::get_launch_url($toolid);
93 $toolpath = $this->strip_base_url($toolpath);
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
);
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);
105 $this->product
= new Item(
109 helper
::get_proxy_url($tool),
113 $requiredmessages = [
115 'basic-lti-launch-request',
119 'CourseSection.title',
120 'CourseSection.label',
121 'CourseSection.sourcedId',
122 'CourseSection.longDescription',
123 'CourseSection.timeFrame.begin',
125 'ResourceLink.title',
126 'ResourceLink.description',
131 'Person.name.family',
132 'Person.email.primary',
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',
148 $optionalmessages = [
151 $this->resourceHandlers
[] = new ResourceHandler(
154 helper
::get_name($tool),
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']);
167 * Override onError for custom error handling.
170 protected function onError() {
173 $message = $this->message
;
174 if ($this->debugMode
&& !empty($this->reason
)) {
175 $message = $this->reason
;
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');
185 * Override onLaunch with tool logic.
188 protected function onLaunch() {
189 global $DB, $SESSION, $CFG;
191 // Check for valid consumer.
192 if (empty($this->consumer
) ||
$this->dataConnector
->loadToolConsumer($this->consumer
) === false) {
194 $this->message
= get_string('invalidtoolconsumer', 'enrol_lti');
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;
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;
219 if (!$correctlaunchurl) {
221 $this->message
= get_string('invalidrequest', 'enrol_lti');
225 // Before we do anything check that the context is valid.
227 $context = context
::instance_by_id($tool->contextid
);
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
;
235 $user->firstname
= $this->user
->getRecordId();
237 if (!empty($this->user
->lastname
)) {
238 $user->lastname
= $this->user
->lastname
;
240 $user->lastname
= $this->tool
->contextid
;
243 $user->email
= core_user
::clean_field($this->user
->email
, 'email');
245 // Get the user data from the LTI consumer.
246 $user = helper
::assign_user_tool_data($tool, $user);
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";
257 $user->id
= \
user_create_user($user);
259 // Get the updated user record.
260 $user = $DB->get_record('user', ['id' => $user->id
]);
262 if (helper
::user_match($user, $dbuser)) {
265 // If email is empty remove it, so we don't update the user with an empty email.
266 if (empty($user->email
)) {
270 $user->id
= $dbuser->id
;
271 \
user_update_user($user);
273 // Get the updated user record.
274 $user = $DB->get_record('user', ['id' => $user->id
]);
278 // Update user image.
279 if (isset($this->user
) && isset($this->user
->image
) && !empty($this->user
->image
)) {
280 $image = $this->user
->image
;
282 // Use custom_user_image parameter as a fallback.
283 $image = $this->resourceLink
->getSetting('custom_user_image');
286 // Check if there is an image to process.
288 helper
::update_user_profile_image($user->id
, $image);
291 // Check if we need to force the page layout to embedded.
292 $isforceembed = $this->resourceLink
->getSetting('custom_force_embed') == 1;
294 // Check if we are an instructor.
295 $isinstructor = $this->user
->isStaff() ||
$this->user
->isAdmin();
297 if ($context->contextlevel
== CONTEXT_COURSE
) {
298 $courseid = $context->instanceid
;
299 $urltogo = new moodle_url('/course/view.php', ['id' => $courseid]);
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
]);
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;
310 print_error('invalidcontext');
314 // Force page layout to embedded if necessary.
316 $SESSION->forcepagelayout
= 'embedded';
318 // May still be set from previous session, so unset it.
319 unset($SESSION->forcepagelayout
);
322 // Enrol the user in the course with no role.
323 $result = helper
::enrol_user($tool, $user->id
);
325 // Display an error, if there is one.
326 if ($result !== helper
::ENROLMENT_SUCCESSFUL
) {
327 print_error($result, 'enrol_lti');
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
);
336 $sourceid = $this->user
->ltiResultSourcedId
;
337 $serviceurl = $this->resourceLink
->getSetting('lis_outcome_service_url');
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;
344 if ($userlog->serviceurl
!= $serviceurl) {
345 $userlog->serviceurl
= $serviceurl;
347 $userlog->lastaccess
= time();
348 $DB->update_record('enrol_lti_users', $userlog);
350 // Add the user details so we can use it later when syncing grades and members.
351 $userlog = new stdClass();
352 $userlog->userid
= $user->id
;
353 $userlog->toolid
= $tool->id
;
354 $userlog->serviceurl
= $serviceurl;
355 $userlog->sourceid
= $sourceid;
356 $userlog->consumerkey
= $this->consumer
->getKey();
357 $userlog->consumersecret
= $tool->secret
;
358 $userlog->lastgrade
= 0;
359 $userlog->lastaccess
= time();
360 $userlog->timecreated
= time();
361 $userlog->membershipsurl
= $this->resourceLink
->getSetting('ext_ims_lis_memberships_url');
362 $userlog->membershipsid
= $this->resourceLink
->getSetting('ext_ims_lis_memberships_id');
364 $DB->insert_record('enrol_lti_users', $userlog);
367 // Finalise the user log in.
368 complete_user_login($user);
370 // Everything's good. Set appropriate OK flag and message values.
372 $this->message
= get_string('success');
374 if (empty($CFG->allowframembedding
)) {
375 // Provide an alternative link.
376 $stropentool = get_string('opentool', 'enrol_lti');
377 echo html_writer
::tag('p', get_string('frameembeddingnotenabled', 'enrol_lti'));
378 echo html_writer
::link($urltogo, $stropentool, ['target' => '_blank']);
380 // All done, redirect the user to where they want to go.
386 * Override onRegister with registration code.
388 protected function onRegister() {
391 if (empty($this->consumer
)) {
393 $this->message
= get_string('invalidtoolconsumer', 'enrol_lti');
397 if (empty($this->returnUrl
)) {
399 $this->message
= get_string('returnurlnotset', 'enrol_lti');
403 if ($this->doToolProxyService()) {
404 // Map tool consumer and published tool, if necessary.
405 $this->map_tool_to_consumer();
407 // Indicate successful processing in message.
408 $this->message
= get_string('successfulregistration', 'enrol_lti');
411 $returnurl = new moodle_url($this->returnUrl
);
412 $returnurl->param('lti_msg', get_string("successfulregistration", "enrol_lti"));
413 $returnurl->param('status', 'success');
414 $guid = $this->consumer
->getKey();
415 $returnurl->param('tool_proxy_guid', $guid);
417 $returnurlout = $returnurl->out(false);
419 $registration = new registration($returnurlout);
420 $output = $PAGE->get_renderer('enrol_lti');
421 echo $output->render($registration);
424 // Tell the consumer that the registration failed.
426 $this->message
= get_string('couldnotestablishproxy', 'enrol_lti');
431 * Performs mapping of the tool consumer to a published tool.
433 * @throws moodle_exception
435 public function map_tool_to_consumer() {
438 if (empty($this->consumer
)) {
439 throw new moodle_exception('invalidtoolconsumer', 'enrol_lti');
442 // Map the consumer to the tool.
444 'toolid' => $this->tool
->id
,
445 'consumerid' => $this->consumer
->getRecordId()
447 $mappingexists = $DB->record_exists('enrol_lti_tool_consumer_map', $mappingparams);
448 if (!$mappingexists) {
449 $DB->insert_record('enrol_lti_tool_consumer_map', (object) $mappingparams);