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 39 and 401] [Versions 401 and 403]

   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 web service endpoints
  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  define('NO_DEBUG_DISPLAY', true);
  27  define('NO_MOODLE_COOKIES', true);
  28  
  29  require_once(__DIR__ . "/../../config.php");
  30  require_once($CFG->dirroot.'/mod/lti/locallib.php');
  31  require_once($CFG->dirroot.'/mod/lti/servicelib.php');
  32  
  33  // TODO: Switch to core oauthlib once implemented - MDL-30149.
  34  use mod_lti\service_exception_handler;
  35  use moodle\mod\lti as lti;
  36  use ltiservice_basicoutcomes\local\service\basicoutcomes;
  37  
  38  $rawbody = file_get_contents("php://input");
  39  
  40  $logrequests  = lti_should_log_request($rawbody);
  41  $errorhandler = new service_exception_handler($logrequests);
  42  
  43  // Register our own error handler so we can always send valid XML response.
  44  set_exception_handler(array($errorhandler, 'handle'));
  45  
  46  if ($logrequests) {
  47      lti_log_request($rawbody);
  48  }
  49  
  50  $ok = true;
  51  $type = null;
  52  $toolproxy = false;
  53  
  54  $consumerkey = lti\get_oauth_key_from_headers(null, array(basicoutcomes::SCOPE_BASIC_OUTCOMES));
  55  if ($consumerkey === false) {
  56      throw new Exception('Missing or invalid consumer key or access token.');
  57  } else if (is_string($consumerkey)) {
  58      $toolproxy = lti_get_tool_proxy_from_guid($consumerkey);
  59      if ($toolproxy !== false) {
  60          $secrets = array($toolproxy->secret);
  61      } else if (!empty($tool)) {
  62          $secrets = array($typeconfig['password']);
  63      } else {
  64          $secrets = lti_get_shared_secrets_by_key($consumerkey);
  65      }
  66      $sharedsecret = lti_verify_message($consumerkey, lti_get_shared_secrets_by_key($consumerkey), $rawbody);
  67      if ($sharedsecret === false) {
  68          throw new Exception('Message signature not valid');
  69      }
  70  }
  71  
  72  // TODO MDL-46023 Replace this code with a call to the new library.
  73  $origentity = lti_libxml_disable_entity_loader(true);
  74  $xml = simplexml_load_string($rawbody);
  75  if (!$xml) {
  76      lti_libxml_disable_entity_loader($origentity);
  77      throw new Exception('Invalid XML content');
  78  }
  79  lti_libxml_disable_entity_loader($origentity);
  80  
  81  $body = $xml->imsx_POXBody;
  82  foreach ($body->children() as $child) {
  83      $messagetype = $child->getName();
  84  }
  85  
  86  // We know more about the message, update error handler to send better errors.
  87  $errorhandler->set_message_id(lti_parse_message_id($xml));
  88  $errorhandler->set_message_type($messagetype);
  89  
  90  switch ($messagetype) {
  91      case 'replaceResultRequest':
  92          $parsed = lti_parse_grade_replace_message($xml);
  93  
  94          $ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid));
  95  
  96          if (!lti_accepts_grades($ltiinstance)) {
  97              throw new Exception('Tool does not accept grades');
  98          }
  99  
 100          lti_verify_sourcedid($ltiinstance, $parsed);
 101          lti_set_session_user($parsed->userid);
 102  
 103          $gradestatus = lti_update_grade($ltiinstance, $parsed->userid, $parsed->launchid, $parsed->gradeval);
 104  
 105          if (!$gradestatus) {
 106              throw new Exception('Grade replace response');
 107          }
 108  
 109          $responsexml = lti_get_response_xml(
 110                  'success',
 111                  'Grade replace response',
 112                  $parsed->messageid,
 113                  'replaceResultResponse'
 114          );
 115  
 116          echo $responsexml->asXML();
 117  
 118          break;
 119  
 120      case 'readResultRequest':
 121          $parsed = lti_parse_grade_read_message($xml);
 122  
 123          $ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid));
 124  
 125          if (!lti_accepts_grades($ltiinstance)) {
 126              throw new Exception('Tool does not accept grades');
 127          }
 128  
 129          // Getting the grade requires the context is set.
 130          $context = context_course::instance($ltiinstance->course);
 131          $PAGE->set_context($context);
 132  
 133          lti_verify_sourcedid($ltiinstance, $parsed);
 134  
 135          $grade = lti_read_grade($ltiinstance, $parsed->userid);
 136  
 137          $responsexml = lti_get_response_xml(
 138                  'success',  // Empty grade is also 'success'.
 139                  'Result read',
 140                  $parsed->messageid,
 141                  'readResultResponse'
 142          );
 143  
 144          $node = $responsexml->imsx_POXBody->readResultResponse;
 145          $node = $node->addChild('result')->addChild('resultScore');
 146          $node->addChild('language', 'en');
 147          $node->addChild('textString', isset($grade) ? $grade : '');
 148  
 149          echo $responsexml->asXML();
 150  
 151          break;
 152  
 153      case 'deleteResultRequest':
 154          $parsed = lti_parse_grade_delete_message($xml);
 155  
 156          $ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid));
 157  
 158          if (!lti_accepts_grades($ltiinstance)) {
 159              throw new Exception('Tool does not accept grades');
 160          }
 161  
 162          lti_verify_sourcedid($ltiinstance, $parsed);
 163          lti_set_session_user($parsed->userid);
 164  
 165          $gradestatus = lti_delete_grade($ltiinstance, $parsed->userid);
 166  
 167          if (!$gradestatus) {
 168              throw new Exception('Grade delete request');
 169          }
 170  
 171          $responsexml = lti_get_response_xml(
 172                  'success',
 173                  'Grade delete request',
 174                  $parsed->messageid,
 175                  'deleteResultResponse'
 176          );
 177  
 178          echo $responsexml->asXML();
 179  
 180          break;
 181  
 182      default:
 183          // Fire an event if we get a web service request which we don't support directly.
 184          // This will allow others to extend the LTI services, which I expect to be a common
 185          // use case, at least until the spec matures.
 186          $data = new stdClass();
 187          $data->body = $rawbody;
 188          $data->xml = $xml;
 189          $data->messageid = lti_parse_message_id($xml);
 190          $data->messagetype = $messagetype;
 191          $data->consumerkey = $consumerkey;
 192          $data->sharedsecret = $sharedsecret;
 193          $eventdata = array();
 194          $eventdata['other'] = array();
 195          $eventdata['other']['messageid'] = $data->messageid;
 196          $eventdata['other']['messagetype'] = $messagetype;
 197          $eventdata['other']['consumerkey'] = $consumerkey;
 198  
 199          // Before firing the event, allow subplugins a chance to handle.
 200          if (lti_extend_lti_services($data)) {
 201              break;
 202          }
 203  
 204          // If an event handler handles the web service, it should set this global to true
 205          // So this code knows whether to send an "operation not supported" or not.
 206          global $ltiwebservicehandled;
 207          $ltiwebservicehandled = false;
 208  
 209          try {
 210              $event = \mod_lti\event\unknown_service_api_called::create($eventdata);
 211              $event->set_message_data($data);
 212              $event->trigger();
 213          } catch (Exception $e) {
 214              $ltiwebservicehandled = false;
 215          }
 216  
 217          if (!$ltiwebservicehandled) {
 218              $responsexml = lti_get_response_xml(
 219                  'unsupported',
 220                  'unsupported',
 221                   lti_parse_message_id($xml),
 222                   $messagetype
 223              );
 224  
 225              echo $responsexml->asXML();
 226          }
 227  
 228          break;
 229  }