Merge branch 'MDL-61960-master' of git://github.com/farhan6318/moodle
[moodle.git] / enrol / lti / classes / tool_provider.php
blob0e181acd7ac05a9b963e475ff8b4b11458823955
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.
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/>.
17 /**
18 * Extends the IMS Tool provider library for the LTI enrolment.
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
25 namespace enrol_lti;
27 defined('MOODLE_INTERNAL') || die;
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;
43 require_once($CFG->dirroot . '/user/lib.php');
45 /**
46 * Extends the IMS Tool provider library for the LTI enrolment.
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
52 class tool_provider extends ToolProvider {
54 /**
55 * @var stdClass $tool The object representing the enrol instance providing this LTI tool
57 protected $tool;
59 /**
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));
69 return null;
72 /**
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) {
78 global $CFG, $SITE;
80 $token = helper::generate_proxy_token($toolid);
82 $tool = helper::get_lti_tool($toolid);
83 $this->tool = $tool;
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(
106 $token,
107 $name,
108 $description,
109 helper::get_proxy_url($tool),
110 '1.0'
113 $requiredmessages = [
114 new Message(
115 'basic-lti-launch-request',
116 $toolpath,
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'
148 $optionalmessages = [
151 $this->resourceHandlers[] = new ResourceHandler(
152 new Item(
153 $token,
154 helper::get_name($tool),
155 $description
157 $icon,
158 $requiredmessages,
159 $optionalmessages
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.
168 * @return void
170 protected function onError() {
171 global $OUTPUT;
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.
186 * @return void
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) {
193 $this->ok = false;
194 $this->message = get_string('invalidtoolconsumer', 'enrol_lti');
195 return;
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;
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) {
220 $this->ok = false;
221 $this->message = get_string('invalidrequest', 'enrol_lti');
222 return;
225 // Before we do anything check that the context is valid.
226 $tool = $this->tool;
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;
234 } else {
235 $user->firstname = $this->user->getRecordId();
237 if (!empty($this->user->lastname)) {
238 $user->lastname = $this->user->lastname;
239 } else {
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";
256 $user->auth = 'lti';
257 $user->id = \user_create_user($user);
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);
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;
281 } else {
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.
287 if ($image) {
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;
309 } else {
310 print_error('invalidcontext');
311 exit();
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);
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');
328 exit();
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);
335 // Login user.
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);
349 } else {
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.
371 $this->ok = true;
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']);
379 } else {
380 // All done, redirect the user to where they want to go.
381 redirect($urltogo);
386 * Override onRegister with registration code.
388 protected function onRegister() {
389 global $PAGE;
391 if (empty($this->consumer)) {
392 $this->ok = false;
393 $this->message = get_string('invalidtoolconsumer', 'enrol_lti');
394 return;
397 if (empty($this->returnUrl)) {
398 $this->ok = false;
399 $this->message = get_string('returnurlnotset', 'enrol_lti');
400 return;
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');
410 // Prepare response.
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);
423 } else {
424 // Tell the consumer that the registration failed.
425 $this->ok = false;
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() {
436 global $DB;
438 if (empty($this->consumer)) {
439 throw new moodle_exception('invalidtoolconsumer', 'enrol_lti');
442 // Map the consumer to the tool.
443 $mappingparams = [
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);