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   * This file contains a class definition for the LISResult container resource
  19   *
  20   * @package    ltiservice_gradebookservices
  21   * @copyright  2017 Cengage Learning http://www.cengage.com
  22   * @author     Dirk Singels, Diego del Blanco, Claude Vervoort
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace ltiservice_gradebookservices\local\resources;
  27  
  28  use ltiservice_gradebookservices\local\service\gradebookservices;
  29  use mod_lti\local\ltiservice\resource_base;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  /**
  34   * A resource implementing LISResult container.
  35   *
  36   * @package    ltiservice_gradebookservices
  37   * @copyright  2017 Cengage Learning http://www.cengage.com
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class scores extends resource_base {
  41  
  42      /**
  43       * Class constructor.
  44       *
  45       * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance
  46       */
  47      public function __construct($service) {
  48  
  49          parent::__construct($service);
  50          $this->id = 'Score.collection';
  51          $this->template = '/{context_id}/lineitems/{item_id}/lineitem/scores';
  52          $this->variables[] = 'Scores.url';
  53          $this->formats[] = 'application/vnd.ims.lis.v1.scorecontainer+json';
  54          $this->formats[] = 'application/vnd.ims.lis.v1.score+json';
  55          $this->methods[] = 'POST';
  56  
  57      }
  58  
  59      /**
  60       * Execute the request for this resource.
  61       *
  62       * @param \mod_lti\local\ltiservice\response $response  Response object for this request.
  63       */
  64      public function execute($response) {
  65          global $CFG, $DB;
  66  
  67          $params = $this->parse_template();
  68          $contextid = $params['context_id'];
  69          $itemid = $params['item_id'];
  70  
  71          // GET is disabled by the moment, but we have the code ready
  72          // for a future implementation.
  73  
  74          $isget = $response->get_request_method() === 'GET';
  75          if ($isget) {
  76              $contenttype = $response->get_accept();
  77          } else {
  78              $contenttype = $response->get_content_type();
  79          }
  80          $container = empty($contenttype) || ($contenttype === $this->formats[0]);
  81          // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2.
  82          $typeid = optional_param('type_id', null, PARAM_ALPHANUM);
  83  
  84          $scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_SCORE;
  85  
  86          try {
  87              if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) {
  88                  throw new \Exception(null, 401);
  89              }
  90              $typeid = $this->get_service()->get_type()->id;
  91              if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) ||
  92                      (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
  93                  throw new \Exception('No context or unsupported content type', 400);
  94              }
  95              if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
  96                  throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
  97              }
  98              if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
  99                  throw new \Exception('Not allowed in context', 403);
 100              }
 101              if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
 102                  throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404);
 103              }
 104              $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
 105              if ($item === false) {
 106                  throw new \Exception('Line item does not exist', 404);
 107              }
 108              $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
 109              $ltilinkid = null;
 110              if (isset($item->iteminstance)) {
 111                  $ltilinkid = $item->iteminstance;
 112              } else if ($gbs && isset($gbs->ltilinkid)) {
 113                  $ltilinkid = $gbs->ltilinkid;
 114              }
 115              if ($ltilinkid != null) {
 116                  if (is_null($typeid)) {
 117                      if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
 118                              $this->get_service()->get_tool_proxy()->id))) {
 119                          $response->set_code(403);
 120                          $response->set_reason("Invalid LTI id supplied.");
 121                          return;
 122                      }
 123                  } else {
 124                      if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
 125                              $typeid))) {
 126                          $response->set_code(403);
 127                          $response->set_reason("Invalid LTI id supplied.");
 128                          return;
 129                      }
 130                  }
 131              }
 132              $json = '[]';
 133              require_once($CFG->libdir.'/gradelib.php');
 134              switch ($response->get_request_method()) {
 135                  case 'GET':
 136                      $response->set_code(405);
 137                      $response->set_reason("GET requests are not allowed.");
 138                      break;
 139                  case 'POST':
 140                      try {
 141                          $json = $this->get_json_for_post_request($response, $response->get_request_data(), $item, $contextid,
 142                              $typeid);
 143                          $response->set_content_type($this->formats[1]);
 144                      } catch (\Exception $e) {
 145                          $response->set_code($e->getCode());
 146                          $response->set_reason($e->getMessage());
 147                      }
 148                      break;
 149                  default:  // Should not be possible.
 150                      $response->set_code(405);
 151                      $response->set_reason("Invalid request method specified.");
 152                      return;
 153              }
 154              $response->set_body($json);
 155          } catch (\Exception $e) {
 156              $response->set_code($e->getCode());
 157              $response->set_reason($e->getMessage());
 158          }
 159      }
 160  
 161      /**
 162       * Generate the JSON for a POST request.
 163       *
 164       * @param \mod_lti\local\ltiservice\response $response Response object for this request.
 165       * @param string $body POST body
 166       * @param object $item Grade item instance
 167       * @param string $contextid
 168       * @param string $typeid
 169       *
 170       * @throws \Exception
 171       */
 172      private function get_json_for_post_request($response, $body, $item, $contextid, $typeid) {
 173          $score = json_decode($body);
 174          if (empty($score) ||
 175                  !isset($score->userId) ||
 176                  !isset($score->timestamp) ||
 177                  !isset($score->gradingProgress) ||
 178                  !isset($score->activityProgress) ||
 179                  !isset($score->timestamp) ||
 180                  isset($score->timestamp) && !gradebookservices::validate_iso8601_date($score->timestamp) ||
 181                  (isset($score->scoreGiven) && !is_numeric($score->scoreGiven)) ||
 182                  (isset($score->scoreGiven) && !isset($score->scoreMaximum)) ||
 183                  (isset($score->scoreMaximum) && !is_numeric($score->scoreMaximum)) ||
 184                  (!gradebookservices::is_user_gradable_in_course($contextid, $score->userId))
 185                  ) {
 186              throw new \Exception('Incorrect score received' . $body, 400);
 187          }
 188          $score->timemodified = intval($score->timestamp);
 189  
 190          if (!isset($score->scoreMaximum)) {
 191              $score->scoreMaximum = 1;
 192          }
 193          $response->set_code(200);
 194          $grade = \grade_grade::fetch(array('itemid' => $item->id, 'userid' => $score->userId));
 195          if ($grade &&  !empty($grade->timemodified)) {
 196              if ($grade->timemodified >= strtotime($score->timestamp)) {
 197                  $exmsg = "Refusing score with an earlier timestamp for item " . $item->id . " and user " . $score->userId;
 198                  throw new \Exception($exmsg, 409);
 199              }
 200          }
 201          if (isset($score->scoreGiven)) {
 202              if ($score->gradingProgress != 'FullyGraded') {
 203                  $score->scoreGiven = null;
 204              }
 205          }
 206          $this->get_service()->save_grade_item($item, $score, $score->userId);
 207      }
 208  
 209      /**
 210       * Parse a value for custom parameter substitution variables.
 211       *
 212       * @param string $value String to be parsed
 213       *
 214       * @return string
 215       */
 216      public function parse_value($value) {
 217          global $COURSE, $CFG;
 218  
 219          if (strpos($value, '$Scores.url') !== false) {
 220              require_once($CFG->libdir . '/gradelib.php');
 221  
 222              $resolved = '';
 223              $this->params['context_id'] = $COURSE->id;
 224              $id = optional_param('id', 0, PARAM_INT); // Course Module ID.
 225              if (!empty($id)) {
 226                  $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
 227                  $id = $cm->instance;
 228                  $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id);
 229                  if ($item && $item->items) {
 230                      $this->params['item_id'] = $item->items[0]->id;
 231                      $resolved = parent::get_endpoint();
 232                  }
 233              }
 234              $value = str_replace('$Scores.url', $resolved, $value);
 235          }
 236  
 237          return $value;
 238      }
 239  }