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 LISResults 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 results 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 = 'Result.collection';
  51          $this->template = '/{context_id}/lineitems/{item_id}/lineitem/results';
  52          $this->variables[] = 'Results.url';
  53          $this->formats[] = 'application/vnd.ims.lis.v2.resultcontainer+json';
  54          $this->methods[] = 'GET';
  55      }
  56  
  57      /**
  58       * Execute the request for this resource.
  59       *
  60       * @param \mod_lti\local\ltiservice\response $response  Response object for this request.
  61       */
  62      public function execute($response) {
  63          global $CFG, $DB;
  64  
  65          $params = $this->parse_template();
  66          $contextid = $params['context_id'];
  67          $itemid = $params['item_id'];
  68  
  69          $isget = $response->get_request_method() === self::HTTP_GET;
  70          // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2.
  71          $typeid = optional_param('type_id', null, PARAM_INT);
  72  
  73          $scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_RESULT_READ;
  74  
  75          try {
  76              if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) {
  77                  throw new \Exception(null, 401);
  78              }
  79              $typeid = $this->get_service()->get_type()->id;
  80              if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
  81                  throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
  82              }
  83              if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
  84                  throw new \Exception('Not allowed in context', 403);
  85              }
  86              if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
  87                  throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404);
  88              }
  89              $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
  90              if ($item === false) {
  91                  throw new \Exception('Line item does not exist', 404);
  92              }
  93              $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
  94              $ltilinkid = null;
  95              if (isset($item->iteminstance)) {
  96                  $ltilinkid = $item->iteminstance;
  97              } else if ($gbs && isset($gbs->ltilinkid)) {
  98                  $ltilinkid = $gbs->ltilinkid;
  99              }
 100              if ($ltilinkid != null) {
 101                  if (is_null($typeid)) {
 102                      if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
 103                              $this->get_service()->get_tool_proxy()->id))) {
 104                          $response->set_code(403);
 105                          $response->set_reason("Invalid LTI id supplied.");
 106                          return;
 107                      }
 108                  } else {
 109                      if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
 110                              $typeid))) {
 111                          $response->set_code(403);
 112                          $response->set_reason("Invalid LTI id supplied.");
 113                          return;
 114                      }
 115                  }
 116              }
 117              require_once($CFG->libdir.'/gradelib.php');
 118              switch ($response->get_request_method()) {
 119                  case 'GET':
 120                      $useridfilter = optional_param('user_id', 0, PARAM_INT);
 121                      $limitnum = optional_param('limit', 0, PARAM_INT);
 122                      $limitfrom = optional_param('from', 0, PARAM_INT);
 123                      $typeid = optional_param('type_id', null, PARAM_TEXT);
 124                      $json = $this->get_json_for_get_request($item->id, $limitfrom, $limitnum,
 125                              $useridfilter, $typeid, $response);
 126                      $response->set_content_type($this->formats[0]);
 127                      $response->set_body($json);
 128                      break;
 129                  default:  // Should not be possible.
 130                      $response->set_code(405);
 131                      $response->set_reason("Invalid request method specified.");
 132                      return;
 133              }
 134              $response->set_body($json);
 135          } catch (\Exception $e) {
 136              $response->set_code($e->getCode());
 137              $response->set_reason($e->getMessage());
 138          }
 139      }
 140  
 141      /**
 142       * Generate the JSON for a GET request.
 143       *
 144       * @param int    $itemid     Grade item instance ID
 145       * @param int $limitfrom  Offset for the first result to include in this paged set
 146       * @param int $limitnum   Maximum number of results to include in the response, ignored if zero
 147       * @param int    $useridfilter     The user id to filter the results.
 148       * @param int    $typeid     Lti tool typeid (or null)
 149       * @param \mod_lti\local\ltiservice\response $response   The response element needed to add a header.
 150       *
 151       * @return string
 152       */
 153      private function get_json_for_get_request($itemid, $limitfrom, $limitnum, $useridfilter, $typeid, $response) {
 154  
 155          if ($useridfilter > 0) {
 156              $grades = \grade_grade::fetch_all(array('itemid' => $itemid, 'userid' => $useridfilter));
 157          } else {
 158              $grades = \grade_grade::fetch_all(array('itemid' => $itemid));
 159          }
 160  
 161          $firstpage = null;
 162          $nextpage = null;
 163          $prevpage = null;
 164          $lastpage = null;
 165          if ($grades && isset($limitnum) && $limitnum > 0) {
 166              // Since we only display grades that have been modified, we need to filter first in order to support
 167              // paging.
 168              $resultgrades = array_filter($grades, function ($grade) {
 169                  return !empty($grade->timemodified);
 170              });
 171              // We save the total count to calculate the last page.
 172              $totalcount = count($resultgrades);
 173              // We slice to the requested item offset to insure proper item is always first, and we always return
 174              // first pageset of any remaining items.
 175              $grades = array_slice($resultgrades, $limitfrom);
 176              if (count($grades) > 0) {
 177                  $pagedgrades = array_chunk($grades, $limitnum);
 178                  $pageset = 0;
 179                  $grades = $pagedgrades[$pageset];
 180              }
 181              if ($limitfrom >= $totalcount || $limitfrom < 0) {
 182                  $outofrange = true;
 183              } else {
 184                  $outofrange = false;
 185              }
 186              $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0;
 187              $limitcurrent = $limitfrom;
 188              $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0;
 189              $limitfrom += $limitnum;
 190  
 191              $baseurl = new \moodle_url($this->get_endpoint());
 192              if (is_null($typeid)) {
 193                  $baseurl->param('limit', $limitnum);
 194  
 195                  if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
 196                      $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
 197                  }
 198                  $firstpage = new \moodle_url($baseurl, ['from' => 0]);
 199                  $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
 200                  $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]);
 201                  if (($limitcurrent > 0) && (!$outofrange)) {
 202                      $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
 203                  }
 204              } else {
 205                  $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]);
 206  
 207                  if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
 208                      $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
 209                  }
 210                  $firstpage = new \moodle_url($baseurl, ['from' => 0]);
 211                  $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
 212                  if (($limitcurrent > 0) && (!$outofrange)) {
 213                      $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
 214                  }
 215              }
 216          }
 217  
 218          $jsonresults = [];
 219          $lineitem = new lineitem($this->get_service());
 220          $endpoint = $lineitem->get_endpoint();
 221          if ($grades) {
 222              foreach ($grades as $grade) {
 223                  if (!empty($grade->timemodified)) {
 224                      array_push($jsonresults, gradebookservices::result_for_json($grade, $endpoint, $typeid));
 225                  }
 226              }
 227          }
 228  
 229          if (isset($canonicalpage) && ($canonicalpage)) {
 230              $links = 'Link: <' . $firstpage->out() . '>; rel=“first”';
 231              if (!is_null($prevpage)) {
 232                  $links .= ', <' . $prevpage->out() . '>; rel=“prev”';
 233              }
 234              $links .= ', <' . $canonicalpage->out() . '>; rel=“canonical”';
 235              if (!is_null($nextpage)) {
 236                  $links .= ', <' . $nextpage->out() . '>; rel=“next”';
 237              }
 238              $links .= ', <' . $lastpage->out() . '>; rel=“last”';
 239              $response->add_additional_header($links);
 240          }
 241          return json_encode($jsonresults);
 242      }
 243  
 244      /**
 245       * Parse a value for custom parameter substitution variables.
 246       *
 247       * @param string $value String to be parsed
 248       *
 249       * @return string
 250       */
 251      public function parse_value($value) {
 252          global $COURSE, $CFG;
 253          if (strpos($value, '$Results.url') !== false) {
 254              require_once($CFG->libdir . '/gradelib.php');
 255  
 256              $resolved = '';
 257              $this->params['context_id'] = $COURSE->id;
 258              $id = optional_param('id', 0, PARAM_INT); // Course Module ID.
 259              if (!empty($id)) {
 260                  $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
 261                  $id = $cm->instance;
 262                  $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id);
 263                  if ($item && $item->items) {
 264                      $this->params['item_id'] = $item->items[0]->id;
 265                      $resolved = parent::get_endpoint();
 266                  }
 267              }
 268              $value = str_replace('$Results.url', $resolved, $value);
 269          }
 270          return $value;
 271      }
 272  }