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.
   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   * LTI enrolment plugin helper.
  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;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * LTI enrolment plugin helper class.
  31   *
  32   * @package enrol_lti
  33   * @copyright 2016 Mark Nelson <markn@moodle.com>
  34   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class helper {
  37      /*
  38       * The value used when we want to enrol new members and unenrol old ones.
  39       */
  40      const MEMBER_SYNC_ENROL_AND_UNENROL = 1;
  41  
  42      /*
  43       * The value used when we want to enrol new members only.
  44       */
  45      const MEMBER_SYNC_ENROL_NEW = 2;
  46  
  47      /*
  48       * The value used when we want to unenrol missing users.
  49       */
  50      const MEMBER_SYNC_UNENROL_MISSING = 3;
  51  
  52      /**
  53       * Code for when an enrolment was successful.
  54       */
  55      const ENROLMENT_SUCCESSFUL = true;
  56  
  57      /**
  58       * Error code for enrolment when max enrolled reached.
  59       */
  60      const ENROLMENT_MAX_ENROLLED = 'maxenrolledreached';
  61  
  62      /**
  63       * Error code for enrolment has not started.
  64       */
  65      const ENROLMENT_NOT_STARTED = 'enrolmentnotstarted';
  66  
  67      /**
  68       * Error code for enrolment when enrolment has finished.
  69       */
  70      const ENROLMENT_FINISHED = 'enrolmentfinished';
  71  
  72      /**
  73       * Error code for when an image file fails to upload.
  74       */
  75      const PROFILE_IMAGE_UPDATE_SUCCESSFUL = true;
  76  
  77      /**
  78       * Error code for when an image file fails to upload.
  79       */
  80      const PROFILE_IMAGE_UPDATE_FAILED = 'profileimagefailed';
  81  
  82      /**
  83       * Creates a unique username.
  84       *
  85       * @param string $consumerkey Consumer key
  86       * @param string $ltiuserid External tool user id
  87       * @return string The new username
  88       */
  89      public static function create_username($consumerkey, $ltiuserid) {
  90          if (!empty($ltiuserid) && !empty($consumerkey)) {
  91              $userkey = $consumerkey . ':' . $ltiuserid;
  92          } else {
  93              $userkey = false;
  94          }
  95  
  96          return 'enrol_lti' . sha1($consumerkey . '::' . $userkey);
  97      }
  98  
  99      /**
 100       * Adds default values for the user object based on the tool provided.
 101       *
 102       * @param \stdClass $tool
 103       * @param \stdClass $user
 104       * @return \stdClass The $user class with added default values
 105       */
 106      public static function assign_user_tool_data($tool, $user) {
 107          global $CFG;
 108  
 109          $user->city = (!empty($tool->city)) ? $tool->city : "";
 110          $user->country = (!empty($tool->country)) ? $tool->country : "";
 111          $user->institution = (!empty($tool->institution)) ? $tool->institution : "";
 112          $user->timezone = (!empty($tool->timezone)) ? $tool->timezone : "";
 113          if (isset($tool->maildisplay)) {
 114              $user->maildisplay = $tool->maildisplay;
 115          } else if (isset($CFG->defaultpreference_maildisplay)) {
 116              $user->maildisplay = $CFG->defaultpreference_maildisplay;
 117          } else {
 118              $user->maildisplay = 2;
 119          }
 120          $user->mnethostid = $CFG->mnet_localhost_id;
 121          $user->confirmed = 1;
 122          $user->lang = $tool->lang;
 123  
 124          return $user;
 125      }
 126  
 127      /**
 128       * Compares two users.
 129       *
 130       * @param \stdClass $newuser The new user
 131       * @param \stdClass $olduser The old user
 132       * @return bool True if both users are the same
 133       */
 134      public static function user_match($newuser, $olduser) {
 135          if ($newuser->firstname != $olduser->firstname) {
 136              return false;
 137          }
 138          if ($newuser->lastname != $olduser->lastname) {
 139              return false;
 140          }
 141          if ($newuser->email != $olduser->email) {
 142              return false;
 143          }
 144          if ($newuser->city != $olduser->city) {
 145              return false;
 146          }
 147          if ($newuser->country != $olduser->country) {
 148              return false;
 149          }
 150          if ($newuser->institution != $olduser->institution) {
 151              return false;
 152          }
 153          if ($newuser->timezone != $olduser->timezone) {
 154              return false;
 155          }
 156          if ($newuser->maildisplay != $olduser->maildisplay) {
 157              return false;
 158          }
 159          if ($newuser->mnethostid != $olduser->mnethostid) {
 160              return false;
 161          }
 162          if ($newuser->confirmed != $olduser->confirmed) {
 163              return false;
 164          }
 165          if ($newuser->lang != $olduser->lang) {
 166              return false;
 167          }
 168  
 169          return true;
 170      }
 171  
 172      /**
 173       * Updates the users profile image.
 174       *
 175       * @param int $userid the id of the user
 176       * @param string $url the url of the image
 177       * @return bool|string true if successful, else a string explaining why it failed
 178       */
 179      public static function update_user_profile_image($userid, $url) {
 180          global $CFG, $DB;
 181  
 182          require_once($CFG->libdir . '/filelib.php');
 183          require_once($CFG->libdir . '/gdlib.php');
 184  
 185          $fs = get_file_storage();
 186  
 187          $context = \context_user::instance($userid, MUST_EXIST);
 188          $fs->delete_area_files($context->id, 'user', 'newicon');
 189  
 190          $filerecord = array(
 191              'contextid' => $context->id,
 192              'component' => 'user',
 193              'filearea' => 'newicon',
 194              'itemid' => 0,
 195              'filepath' => '/'
 196          );
 197  
 198          $urlparams = array(
 199              'calctimeout' => false,
 200              'timeout' => 5,
 201              'skipcertverify' => true,
 202              'connecttimeout' => 5
 203          );
 204  
 205          try {
 206              $fs->create_file_from_url($filerecord, $url, $urlparams);
 207          } catch (\file_exception $e) {
 208              return get_string($e->errorcode, $e->module, $e->a);
 209          }
 210  
 211          $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false);
 212  
 213          // There should only be one.
 214          $iconfile = reset($iconfile);
 215  
 216          // Something went wrong while creating temp file - remove the uploaded file.
 217          if (!$iconfile = $iconfile->copy_content_to_temp()) {
 218              $fs->delete_area_files($context->id, 'user', 'newicon');
 219              return self::PROFILE_IMAGE_UPDATE_FAILED;
 220          }
 221  
 222          // Copy file to temporary location and the send it for processing icon.
 223          $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile);
 224          // Delete temporary file.
 225          @unlink($iconfile);
 226          // Remove uploaded file.
 227          $fs->delete_area_files($context->id, 'user', 'newicon');
 228          // Set the user's picture.
 229          $DB->set_field('user', 'picture', $newpicture, array('id' => $userid));
 230          return self::PROFILE_IMAGE_UPDATE_SUCCESSFUL;
 231      }
 232  
 233      /**
 234       * Enrol a user in a course.
 235       *
 236       * @param \stdclass $tool The tool object (retrieved using self::get_lti_tool() or self::get_lti_tools())
 237       * @param int $userid The user id
 238       * @return bool|string returns true if successful, else an error code
 239       */
 240      public static function enrol_user($tool, $userid) {
 241          global $DB;
 242  
 243          // Check if the user enrolment exists.
 244          if (!$DB->record_exists('user_enrolments', array('enrolid' => $tool->enrolid, 'userid' => $userid))) {
 245              // Check if the maximum enrolled limit has been met.
 246              if ($tool->maxenrolled) {
 247                  if ($DB->count_records('user_enrolments', array('enrolid' => $tool->enrolid)) >= $tool->maxenrolled) {
 248                      return self::ENROLMENT_MAX_ENROLLED;
 249                  }
 250              }
 251              // Check if the enrolment has not started.
 252              if ($tool->enrolstartdate && time() < $tool->enrolstartdate) {
 253                  return self::ENROLMENT_NOT_STARTED;
 254              }
 255              // Check if the enrolment has finished.
 256              if ($tool->enrolenddate && time() > $tool->enrolenddate) {
 257                  return self::ENROLMENT_FINISHED;
 258              }
 259  
 260              $timeend = 0;
 261              if ($tool->enrolperiod) {
 262                  $timeend = time() + $tool->enrolperiod;
 263              }
 264  
 265              // Finally, enrol the user.
 266              $instance = new \stdClass();
 267              $instance->id = $tool->enrolid;
 268              $instance->courseid = $tool->courseid;
 269              $instance->enrol = 'lti';
 270              $instance->status = $tool->status;
 271              $ltienrol = enrol_get_plugin('lti');
 272  
 273              // Hack - need to do this to workaround DB caching hack. See MDL-53977.
 274              $timestart = intval(substr(time(), 0, 8) . '00') - 1;
 275              $ltienrol->enrol_user($instance, $userid, null, $timestart, $timeend);
 276          }
 277  
 278          return self::ENROLMENT_SUCCESSFUL;
 279      }
 280  
 281      /**
 282       * Returns the LTI tool.
 283       *
 284       * @param int $toolid
 285       * @return \stdClass the tool
 286       */
 287      public static function get_lti_tool($toolid) {
 288          global $DB;
 289  
 290          $sql = "SELECT elt.*, e.name, e.courseid, e.status, e.enrolstartdate, e.enrolenddate, e.enrolperiod
 291                    FROM {enrol_lti_tools} elt
 292                    JOIN {enrol} e
 293                      ON elt.enrolid = e.id
 294                   WHERE elt.id = :tid";
 295  
 296          return $DB->get_record_sql($sql, array('tid' => $toolid), MUST_EXIST);
 297      }
 298  
 299      /**
 300       * Returns the LTI tools requested.
 301       *
 302       * @param array $params The list of SQL params (eg. array('columnname' => value, 'columnname2' => value)).
 303       * @param int $limitfrom return a subset of records, starting at this point (optional).
 304       * @param int $limitnum return a subset comprising this many records in total
 305       * @return array of tools
 306       */
 307      public static function get_lti_tools($params = array(), $limitfrom = 0, $limitnum = 0) {
 308          global $DB;
 309  
 310          $sql = "SELECT elt.*, e.name, e.courseid, e.status, e.enrolstartdate, e.enrolenddate, e.enrolperiod
 311                    FROM {enrol_lti_tools} elt
 312                    JOIN {enrol} e
 313                      ON elt.enrolid = e.id";
 314          if ($params) {
 315              $where = "WHERE";
 316              foreach ($params as $colname => $value) {
 317                  $sql .= " $where $colname = :$colname";
 318                  $where = "AND";
 319              }
 320          }
 321          $sql .= " ORDER BY elt.timecreated";
 322  
 323          return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
 324      }
 325  
 326      /**
 327       * Returns the number of LTI tools.
 328       *
 329       * @param array $params The list of SQL params (eg. array('columnname' => value, 'columnname2' => value)).
 330       * @return int The number of tools
 331       */
 332      public static function count_lti_tools($params = array()) {
 333          global $DB;
 334  
 335          $sql = "SELECT COUNT(*)
 336                    FROM {enrol_lti_tools} elt
 337                    JOIN {enrol} e
 338                      ON elt.enrolid = e.id";
 339          if ($params) {
 340              $where = "WHERE";
 341              foreach ($params as $colname => $value) {
 342                  $sql .= " $where $colname = :$colname";
 343                  $where = "AND";
 344              }
 345          }
 346  
 347          return $DB->count_records_sql($sql, $params);
 348      }
 349  
 350      /**
 351       * Create a IMS POX body request for sync grades.
 352       *
 353       * @param string $source Sourceid required for the request
 354       * @param float $grade User final grade
 355       * @return string
 356       */
 357      public static function create_service_body($source, $grade) {
 358          return '<?xml version="1.0" encoding="UTF-8"?>
 359              <imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
 360                <imsx_POXHeader>
 361                  <imsx_POXRequestHeaderInfo>
 362                    <imsx_version>V1.0</imsx_version>
 363                    <imsx_messageIdentifier>' . (time()) . '</imsx_messageIdentifier>
 364                  </imsx_POXRequestHeaderInfo>
 365                </imsx_POXHeader>
 366                <imsx_POXBody>
 367                  <replaceResultRequest>
 368                    <resultRecord>
 369                      <sourcedGUID>
 370                        <sourcedId>' . $source . '</sourcedId>
 371                      </sourcedGUID>
 372                      <result>
 373                        <resultScore>
 374                          <language>en-us</language>
 375                          <textString>' . $grade . '</textString>
 376                        </resultScore>
 377                      </result>
 378                    </resultRecord>
 379                  </replaceResultRequest>
 380                </imsx_POXBody>
 381              </imsx_POXEnvelopeRequest>';
 382      }
 383  
 384      /**
 385       * Returns the url to launch the lti tool.
 386       *
 387       * @param int $toolid the id of the shared tool
 388       * @return \moodle_url the url to launch the tool
 389       * @since Moodle 3.2
 390       */
 391      public static function get_launch_url($toolid) {
 392          return new \moodle_url('/enrol/lti/tool.php', array('id' => $toolid));
 393      }
 394  
 395      /**
 396       * Returns the name of the lti enrolment instance, or the name of the course/module being shared.
 397       *
 398       * @param \stdClass $tool The lti tool
 399       * @return string The name of the tool
 400       * @since Moodle 3.2
 401       */
 402      public static function get_name($tool) {
 403          $name = null;
 404  
 405          if (empty($tool->name)) {
 406              $toolcontext = \context::instance_by_id($tool->contextid);
 407              $name = $toolcontext->get_context_name();
 408          } else {
 409              $name = $tool->name;
 410          };
 411  
 412          return $name;
 413      }
 414  
 415      /**
 416       * Returns a description of the course or module that this lti instance points to.
 417       *
 418       * @param \stdClass $tool The lti tool
 419       * @return string A description of the tool
 420       * @since Moodle 3.2
 421       */
 422      public static function get_description($tool) {
 423          global $DB;
 424          $description = '';
 425          $context = \context::instance_by_id($tool->contextid);
 426          if ($context->contextlevel == CONTEXT_COURSE) {
 427              $course = $DB->get_record('course', array('id' => $context->instanceid));
 428              $description = $course->summary;
 429          } else if ($context->contextlevel == CONTEXT_MODULE) {
 430              $cmid = $context->instanceid;
 431              $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
 432              $module = $DB->get_record($cm->modname, array('id' => $cm->instance));
 433              $description = $module->intro;
 434          }
 435          return trim(html_to_text($description));
 436      }
 437  
 438      /**
 439       * Returns the icon of the tool.
 440       *
 441       * @param \stdClass $tool The lti tool
 442       * @return \moodle_url A url to the icon of the tool
 443       * @since Moodle 3.2
 444       */
 445      public static function get_icon($tool) {
 446          global $OUTPUT;
 447          return $OUTPUT->favicon();
 448      }
 449  
 450      /**
 451       * Returns the url to the cartridge representing the tool.
 452       *
 453       * If you have slash arguments enabled, this will be a nice url ending in cartridge.xml.
 454       * If not it will be a php page with some parameters passed.
 455       *
 456       * @param \stdClass $tool The lti tool
 457       * @return string The url to the cartridge representing the tool
 458       * @since Moodle 3.2
 459       */
 460      public static function get_cartridge_url($tool) {
 461          global $CFG;
 462          $url = null;
 463  
 464          $id = $tool->id;
 465          $token = self::generate_cartridge_token($tool->id);
 466          if ($CFG->slasharguments) {
 467              $url = new \moodle_url('/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml');
 468          } else {
 469              $url = new \moodle_url('/enrol/lti/cartridge.php',
 470                      array(
 471                          'id' => $id,
 472                          'token' => $token
 473                      )
 474                  );
 475          }
 476          return $url;
 477      }
 478  
 479      /**
 480       * Returns the url to the tool proxy registration url.
 481       *
 482       * If you have slash arguments enabled, this will be a nice url ending in cartridge.xml.
 483       * If not it will be a php page with some parameters passed.
 484       *
 485       * @param \stdClass $tool The lti tool
 486       * @return string The url to the cartridge representing the tool
 487       */
 488      public static function get_proxy_url($tool) {
 489          global $CFG;
 490          $url = null;
 491  
 492          $id = $tool->id;
 493          $token = self::generate_proxy_token($tool->id);
 494          if ($CFG->slasharguments) {
 495              $url = new \moodle_url('/enrol/lti/proxy.php/' . $id . '/' . $token . '/');
 496          } else {
 497              $url = new \moodle_url('/enrol/lti/proxy.php',
 498                      array(
 499                          'id' => $id,
 500                          'token' => $token
 501                      )
 502                  );
 503          }
 504          return $url;
 505      }
 506  
 507      /**
 508       * Returns a unique hash for this site and this enrolment instance.
 509       *
 510       * Used to verify that the link to the cartridge has not just been guessed.
 511       *
 512       * @param int $toolid The id of the shared tool
 513       * @return string MD5 hash of combined site ID and enrolment instance ID.
 514       * @since Moodle 3.2
 515       */
 516      public static function generate_cartridge_token($toolid) {
 517          $siteidentifier = get_site_identifier();
 518          $checkhash = md5($siteidentifier . '_enrol_lti_cartridge_' . $toolid);
 519          return $checkhash;
 520      }
 521  
 522      /**
 523       * Returns a unique hash for this site and this enrolment instance.
 524       *
 525       * Used to verify that the link to the proxy has not just been guessed.
 526       *
 527       * @param int $toolid The id of the shared tool
 528       * @return string MD5 hash of combined site ID and enrolment instance ID.
 529       * @since Moodle 3.2
 530       */
 531      public static function generate_proxy_token($toolid) {
 532          $siteidentifier = get_site_identifier();
 533          $checkhash = md5($siteidentifier . '_enrol_lti_proxy_' . $toolid);
 534          return $checkhash;
 535      }
 536  
 537      /**
 538       * Verifies that the given token matches the cartridge token of the given shared tool.
 539       *
 540       * @param int $toolid The id of the shared tool
 541       * @param string $token hash for this site and this enrolment instance
 542       * @return boolean True if the token matches, false if it does not
 543       * @since Moodle 3.2
 544       */
 545      public static function verify_cartridge_token($toolid, $token) {
 546          return $token == self::generate_cartridge_token($toolid);
 547      }
 548  
 549      /**
 550       * Verifies that the given token matches the proxy token of the given shared tool.
 551       *
 552       * @param int $toolid The id of the shared tool
 553       * @param string $token hash for this site and this enrolment instance
 554       * @return boolean True if the token matches, false if it does not
 555       * @since Moodle 3.2
 556       */
 557      public static function verify_proxy_token($toolid, $token) {
 558          return $token == self::generate_proxy_token($toolid);
 559      }
 560  
 561      /**
 562       * Returns the parameters of the cartridge as an associative array of partial xpath.
 563       *
 564       * @param int $toolid The id of the shared tool
 565       * @return array Recursive associative array with partial xpath to be concatenated into an xpath expression
 566       *     before setting the value.
 567       * @since Moodle 3.2
 568       */
 569      protected static function get_cartridge_parameters($toolid) {
 570          global $PAGE, $SITE;
 571          $PAGE->set_context(\context_system::instance());
 572  
 573          // Get the tool.
 574          $tool = self::get_lti_tool($toolid);
 575  
 576          // Work out the name of the tool.
 577          $title = self::get_name($tool);
 578          $launchurl = self::get_launch_url($toolid);
 579          $launchurl = $launchurl->out(false);
 580          $iconurl = self::get_icon($tool);
 581          $iconurl = $iconurl->out(false);
 582          $securelaunchurl = null;
 583          $secureiconurl = null;
 584          $vendorurl = new \moodle_url('/');
 585          $vendorurl = $vendorurl->out(false);
 586          $description = self::get_description($tool);
 587  
 588          // If we are a https site, we can add the launch url and icon urls as secure equivalents.
 589          if (\is_https()) {
 590              $securelaunchurl = $launchurl;
 591              $secureiconurl = $iconurl;
 592          }
 593  
 594          return array(
 595                  "/cc:cartridge_basiclti_link" => array(
 596                      "/blti:title" => $title,
 597                      "/blti:description" => $description,
 598                      "/blti:extensions" => array(
 599                              "/lticm:property[@name='icon_url']" => $iconurl,
 600                              "/lticm:property[@name='secure_icon_url']" => $secureiconurl
 601                          ),
 602                      "/blti:launch_url" => $launchurl,
 603                      "/blti:secure_launch_url" => $securelaunchurl,
 604                      "/blti:icon" => $iconurl,
 605                      "/blti:secure_icon" => $secureiconurl,
 606                      "/blti:vendor" => array(
 607                              "/lticp:code" => $SITE->shortname,
 608                              "/lticp:name" => $SITE->fullname,
 609                              "/lticp:description" => trim(html_to_text($SITE->summary)),
 610                              "/lticp:url" => $vendorurl
 611                          )
 612                  )
 613              );
 614      }
 615  
 616      /**
 617       * Traverses a recursive associative array, setting the properties of the corresponding
 618       * xpath element.
 619       *
 620       * @param \DOMXPath $xpath The xpath with the xml to modify
 621       * @param array $parameters The array of xpaths to search through
 622       * @param string $prefix The current xpath prefix (gets longer the deeper into the array you go)
 623       * @return void
 624       * @since Moodle 3.2
 625       */
 626      protected static function set_xpath($xpath, $parameters, $prefix = '') {
 627          foreach ($parameters as $key => $value) {
 628              if (is_array($value)) {
 629                  self::set_xpath($xpath, $value, $prefix . $key);
 630              } else {
 631                  $result = @$xpath->query($prefix . $key);
 632                  if ($result) {
 633                      $node = $result->item(0);
 634                      if ($node) {
 635                          if (is_null($value)) {
 636                              $node->parentNode->removeChild($node);
 637                          } else {
 638                              $node->nodeValue = s($value);
 639                          }
 640                      }
 641                  } else {
 642                      throw new \coding_exception('Please check your XPATH and try again.');
 643                  }
 644              }
 645          }
 646      }
 647  
 648      /**
 649       * Create an IMS cartridge for the tool.
 650       *
 651       * @param int $toolid The id of the shared tool
 652       * @return string representing the generated cartridge
 653       * @since Moodle 3.2
 654       */
 655      public static function create_cartridge($toolid) {
 656          $cartridge = new \DOMDocument();
 657          $cartridge->load(realpath(__DIR__ . '/../xml/imslticc.xml'));
 658          $xpath = new \DOMXpath($cartridge);
 659          $xpath->registerNamespace('cc', 'http://www.imsglobal.org/xsd/imslticc_v1p0');
 660          $parameters = self::get_cartridge_parameters($toolid);
 661          self::set_xpath($xpath, $parameters);
 662  
 663          return $cartridge->saveXML();
 664      }
 665  }