Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.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   * Utility code for LTI service handling.
  19   *
  20   * @package mod_lti
  21   * @copyright  Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com)
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   * @author     Chris Scribner
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die;
  27  
  28  require_once($CFG->dirroot.'/mod/lti/OAuthBody.php');
  29  require_once($CFG->dirroot.'/mod/lti/locallib.php');
  30  
  31  // TODO: Switch to core oauthlib once implemented - MDL-30149.
  32  use moodle\mod\lti as lti;
  33  
  34  define('LTI_ITEM_TYPE', 'mod');
  35  define('LTI_ITEM_MODULE', 'lti');
  36  define('LTI_SOURCE', 'mod/lti');
  37  
  38  function lti_get_response_xml($codemajor, $description, $messageref, $messagetype) {
  39      $xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><imsx_POXEnvelopeResponse />');
  40      $xml->addAttribute('xmlns', 'http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0');
  41  
  42      $headerinfo = $xml->addChild('imsx_POXHeader')->addChild('imsx_POXResponseHeaderInfo');
  43  
  44      $headerinfo->addChild('imsx_version', 'V1.0');
  45      $headerinfo->addChild('imsx_messageIdentifier', (string)mt_rand());
  46  
  47      $statusinfo = $headerinfo->addChild('imsx_statusInfo');
  48      $statusinfo->addchild('imsx_codeMajor', $codemajor);
  49      $statusinfo->addChild('imsx_severity', 'status');
  50      $statusinfo->addChild('imsx_description', $description);
  51      $statusinfo->addChild('imsx_messageRefIdentifier', $messageref);
  52      $incomingtype = str_replace('Response', 'Request', $messagetype);
  53      $statusinfo->addChild('imsx_operationRefIdentifier', $incomingtype);
  54  
  55      $xml->addChild('imsx_POXBody')->addChild($messagetype);
  56  
  57      return $xml;
  58  }
  59  
  60  function lti_parse_message_id($xml) {
  61      if (empty($xml->imsx_POXHeader)) {
  62          return '';
  63      }
  64  
  65      $node = $xml->imsx_POXHeader->imsx_POXRequestHeaderInfo->imsx_messageIdentifier;
  66      $messageid = (string)$node;
  67  
  68      return $messageid;
  69  }
  70  
  71  function lti_parse_grade_replace_message($xml) {
  72      $node = $xml->imsx_POXBody->replaceResultRequest->resultRecord->sourcedGUID->sourcedId;
  73      $resultjson = json_decode((string)$node);
  74      if ( is_null($resultjson) ) {
  75          throw new Exception('Invalid sourcedId in result message');
  76      }
  77      $node = $xml->imsx_POXBody->replaceResultRequest->resultRecord->result->resultScore->textString;
  78  
  79      $score = (string) $node;
  80      if ( ! is_numeric($score) ) {
  81          throw new Exception('Score must be numeric');
  82      }
  83      $grade = floatval($score);
  84      if ( $grade < 0.0 || $grade > 1.0 ) {
  85          throw new Exception('Score not between 0.0 and 1.0');
  86      }
  87  
  88      $parsed = new stdClass();
  89      $parsed->gradeval = $grade;
  90  
  91      $parsed->instanceid = $resultjson->data->instanceid;
  92      $parsed->userid = $resultjson->data->userid;
  93      $parsed->launchid = $resultjson->data->launchid;
  94      $parsed->typeid = $resultjson->data->typeid;
  95      $parsed->sourcedidhash = $resultjson->hash;
  96  
  97      $parsed->messageid = lti_parse_message_id($xml);
  98  
  99      return $parsed;
 100  }
 101  
 102  function lti_parse_grade_read_message($xml) {
 103      $node = $xml->imsx_POXBody->readResultRequest->resultRecord->sourcedGUID->sourcedId;
 104      $resultjson = json_decode((string)$node);
 105      if ( is_null($resultjson) ) {
 106          throw new Exception('Invalid sourcedId in result message');
 107      }
 108  
 109      $parsed = new stdClass();
 110      $parsed->instanceid = $resultjson->data->instanceid;
 111      $parsed->userid = $resultjson->data->userid;
 112      $parsed->launchid = $resultjson->data->launchid;
 113      $parsed->typeid = $resultjson->data->typeid;
 114      $parsed->sourcedidhash = $resultjson->hash;
 115  
 116      $parsed->messageid = lti_parse_message_id($xml);
 117  
 118      return $parsed;
 119  }
 120  
 121  function lti_parse_grade_delete_message($xml) {
 122      $node = $xml->imsx_POXBody->deleteResultRequest->resultRecord->sourcedGUID->sourcedId;
 123      $resultjson = json_decode((string)$node);
 124      if ( is_null($resultjson) ) {
 125          throw new Exception('Invalid sourcedId in result message');
 126      }
 127  
 128      $parsed = new stdClass();
 129      $parsed->instanceid = $resultjson->data->instanceid;
 130      $parsed->userid = $resultjson->data->userid;
 131      $parsed->launchid = $resultjson->data->launchid;
 132      $parsed->typeid = $resultjson->data->typeid;
 133      $parsed->sourcedidhash = $resultjson->hash;
 134  
 135      $parsed->messageid = lti_parse_message_id($xml);
 136  
 137      return $parsed;
 138  }
 139  
 140  function lti_accepts_grades($ltiinstance) {
 141      global $DB;
 142  
 143      $acceptsgrades = true;
 144      $ltitype = $DB->get_record('lti_types', array('id' => $ltiinstance->typeid));
 145  
 146      if (empty($ltitype->toolproxyid)) {
 147          $typeconfig = lti_get_config($ltiinstance);
 148  
 149          $typeacceptgrades = isset($typeconfig['acceptgrades']) ? $typeconfig['acceptgrades'] : LTI_SETTING_DELEGATE;
 150  
 151          if (!($typeacceptgrades == LTI_SETTING_ALWAYS ||
 152              ($typeacceptgrades == LTI_SETTING_DELEGATE && $ltiinstance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))) {
 153              $acceptsgrades = false;
 154          }
 155      } else {
 156          $enabledcapabilities = explode("\n", $ltitype->enabledcapability);
 157          $acceptsgrades = in_array('Result.autocreate', $enabledcapabilities) || in_array('BasicOutcome.url', $enabledcapabilities);
 158      }
 159  
 160      return $acceptsgrades;
 161  }
 162  
 163  /**
 164   * Set the passed user ID to the session user.
 165   *
 166   * @param int $userid
 167   */
 168  function lti_set_session_user($userid) {
 169      global $DB;
 170  
 171      if ($user = $DB->get_record('user', array('id' => $userid))) {
 172          \core\session\manager::set_user($user);
 173      }
 174  }
 175  
 176  function lti_update_grade($ltiinstance, $userid, $launchid, $gradeval) {
 177      global $CFG, $DB;
 178      require_once($CFG->libdir . '/gradelib.php');
 179  
 180      $params = array();
 181      $params['itemname'] = $ltiinstance->name;
 182  
 183      $gradeval = $gradeval * floatval($ltiinstance->grade);
 184  
 185      $grade = new stdClass();
 186      $grade->userid   = $userid;
 187      $grade->rawgrade = $gradeval;
 188  
 189      $status = grade_update(LTI_SOURCE, $ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, 0, $grade, $params);
 190  
 191      $record = $DB->get_record('lti_submission', array('ltiid' => $ltiinstance->id, 'userid' => $userid,
 192          'launchid' => $launchid), 'id');
 193      if ($record) {
 194          $id = $record->id;
 195      } else {
 196          $id = null;
 197      }
 198  
 199      if (!empty($id)) {
 200          $DB->update_record('lti_submission', array(
 201              'id' => $id,
 202              'dateupdated' => time(),
 203              'gradepercent' => $gradeval,
 204              'state' => 2
 205          ));
 206      } else {
 207          $DB->insert_record('lti_submission', array(
 208              'ltiid' => $ltiinstance->id,
 209              'userid' => $userid,
 210              'datesubmitted' => time(),
 211              'dateupdated' => time(),
 212              'gradepercent' => $gradeval,
 213              'originalgrade' => $gradeval,
 214              'launchid' => $launchid,
 215              'state' => 1
 216          ));
 217      }
 218  
 219      return $status == GRADE_UPDATE_OK;
 220  }
 221  
 222  function lti_read_grade($ltiinstance, $userid) {
 223      global $CFG;
 224      require_once($CFG->libdir . '/gradelib.php');
 225  
 226      $grades = grade_get_grades($ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, $userid);
 227  
 228      $ltigrade = floatval($ltiinstance->grade);
 229  
 230      if (!empty($ltigrade) && isset($grades) && isset($grades->items[0]) && is_array($grades->items[0]->grades)) {
 231          foreach ($grades->items[0]->grades as $agrade) {
 232              $grade = $agrade->grade;
 233              if (isset($grade)) {
 234                  return $grade / $ltigrade;
 235              }
 236          }
 237      }
 238  }
 239  
 240  function lti_delete_grade($ltiinstance, $userid) {
 241      global $CFG;
 242      require_once($CFG->libdir . '/gradelib.php');
 243  
 244      $grade = new stdClass();
 245      $grade->userid   = $userid;
 246      $grade->rawgrade = null;
 247  
 248      $status = grade_update(LTI_SOURCE, $ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, 0, $grade);
 249  
 250      return $status == GRADE_UPDATE_OK;
 251  }
 252  
 253  function lti_verify_message($key, $sharedsecrets, $body, $headers = null) {
 254      foreach ($sharedsecrets as $secret) {
 255          $signaturefailed = false;
 256  
 257          try {
 258              // TODO: Switch to core oauthlib once implemented - MDL-30149.
 259              lti\handle_oauth_body_post($key, $secret, $body, $headers);
 260          } catch (Exception $e) {
 261              debugging('LTI message verification failed: '.$e->getMessage());
 262              $signaturefailed = true;
 263          }
 264  
 265          if (!$signaturefailed) {
 266              return $secret; // Return the secret used to sign the message).
 267          }
 268      }
 269  
 270      return false;
 271  }
 272  
 273  /**
 274   * Validate source ID from external request
 275   *
 276   * @param object $ltiinstance
 277   * @param object $parsed
 278   * @throws Exception
 279   */
 280  function lti_verify_sourcedid($ltiinstance, $parsed) {
 281      $sourceid = lti_build_sourcedid($parsed->instanceid, $parsed->userid,
 282          $ltiinstance->servicesalt, $parsed->typeid, $parsed->launchid);
 283  
 284      if ($sourceid->hash != $parsed->sourcedidhash) {
 285          throw new Exception('SourcedId hash not valid');
 286      }
 287  }
 288  
 289  /**
 290   * Extend the LTI services through the ltisource plugins
 291   *
 292   * @param stdClass $data LTI request data
 293   * @return bool
 294   * @throws coding_exception
 295   */
 296  function lti_extend_lti_services($data) {
 297      $plugins = get_plugin_list_with_function('ltisource', $data->messagetype);
 298      if (!empty($plugins)) {
 299          // There can only be one.
 300          if (count($plugins) > 1) {
 301              throw new coding_exception('More than one ltisource plugin handler found');
 302          }
 303          $data->xml = new SimpleXMLElement($data->body);
 304          $callback = current($plugins);
 305          call_user_func($callback, $data);
 306  
 307          return true;
 308      }
 309      return false;
 310  }