Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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  }