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 LineItem 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 LineItem 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 lineitems 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 = 'LineItem.collection';
  51          $this->template = '/{context_id}/lineitems';
  52          $this->variables[] = 'LineItems.url';
  53          $this->formats[] = 'application/vnd.ims.lis.v2.lineitemcontainer+json';
  54          $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json';
  55          $this->methods[] = self::HTTP_GET;
  56          $this->methods[] = self::HTTP_POST;
  57  
  58      }
  59  
  60      /**
  61       * Execute the request for this resource.
  62       *
  63       * @param \mod_lti\local\ltiservice\response $response  Response object for this request.
  64       */
  65      public function execute($response) {
  66          global $DB;
  67  
  68          $params = $this->parse_template();
  69          $contextid = $params['context_id'];
  70          $isget = $response->get_request_method() === self::HTTP_GET;
  71          if ($isget) {
  72              $contenttype = $response->get_accept();
  73          } else {
  74              $contenttype = $response->get_content_type();
  75          }
  76          $container = empty($contenttype) || ($contenttype === $this->formats[0]);
  77          // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2.
  78          $typeid = optional_param('type_id', null, PARAM_INT);
  79  
  80          $scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM);
  81          if ($response->get_request_method() === self::HTTP_GET) {
  82              $scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
  83          }
  84  
  85          try {
  86              if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) {
  87                  throw new \Exception(null, 401);
  88              }
  89              $typeid = $this->get_service()->get_type()->id;
  90              if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) ||
  91                      (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
  92                  throw new \Exception('No context or unsupported content type', 400);
  93              }
  94              if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
  95                  throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
  96              }
  97              if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
  98                  throw new \Exception('Not allowed in context', 403);
  99              }
 100              if ($response->get_request_method() !== self::HTTP_POST) {
 101                  $resourceid = optional_param('resource_id', null, PARAM_TEXT);
 102                  $ltilinkid = optional_param('resource_link_id', null, PARAM_TEXT);
 103                  if (is_null($ltilinkid)) {
 104                      $ltilinkid = optional_param('lti_link_id', null, PARAM_TEXT);
 105                  }
 106                  $tag = optional_param('tag', null, PARAM_TEXT);
 107                  $limitnum = optional_param('limit', 0, PARAM_INT);
 108                  $limitfrom = optional_param('from', 0, PARAM_INT);
 109                  $itemsandcount = $this->get_service()->get_lineitems($contextid, $resourceid, $ltilinkid, $tag, $limitfrom,
 110                          $limitnum, $typeid);
 111                  $items = $itemsandcount[1];
 112                  $totalcount = $itemsandcount[0];
 113                  $json = $this->get_json_for_get_request($items, $resourceid, $ltilinkid, $tag, $limitfrom,
 114                          $limitnum, $totalcount, $typeid, $response);
 115                  $response->set_content_type($this->formats[0]);
 116              } else {
 117                  $json = $this->get_json_for_post_request($response->get_request_data(), $contextid, $typeid);
 118                  $response->set_code(201);
 119                  $response->set_content_type($this->formats[1]);
 120              }
 121              $response->set_body($json);
 122  
 123          } catch (\Exception $e) {
 124              $response->set_code($e->getCode());
 125              $response->set_reason($e->getMessage());
 126          }
 127  
 128      }
 129  
 130      /**
 131       * Generate the JSON for a GET request.
 132       *
 133       * @param array $items Array of lineitems
 134       * @param string $resourceid Resource identifier used for filtering, may be null
 135       * @param string $ltilinkid Resource Link identifier used for filtering, may be null
 136       * @param string $tag Tag identifier used for filtering, may be null
 137       * @param int $limitfrom Offset of the first line item to return
 138       * @param int $limitnum Maximum number of line items to return, ignored if zero or less
 139       * @param int $totalcount Number of total lineitems before filtering for paging
 140       * @param int $typeid Maximum number of line items to return, ignored if zero or less
 141       * @param \mod_lti\local\ltiservice\response $response
 142  
 143       * @return string
 144       */
 145      private function get_json_for_get_request($items, $resourceid, $ltilinkid,
 146              $tag, $limitfrom, $limitnum, $totalcount, $typeid, $response) {
 147  
 148          $firstpage = null;
 149          $nextpage = null;
 150          $prevpage = null;
 151          $lastpage = null;
 152          if (isset($limitnum) && $limitnum > 0) {
 153              if ($limitfrom >= $totalcount || $limitfrom < 0) {
 154                  $outofrange = true;
 155              } else {
 156                  $outofrange = false;
 157              }
 158              $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0;
 159              $limitcurrent = $limitfrom;
 160              $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0;
 161              $limitfrom += $limitnum;
 162  
 163              $baseurl = new \moodle_url($this->get_endpoint());
 164              if (isset($resourceid)) {
 165                  $baseurl->param('resource_id', $resourceid);
 166              }
 167              if (isset($ltilinkid)) {
 168                  $baseurl->param('resource_link_id', $ltilinkid);
 169              }
 170              if (isset($tag)) {
 171                  $baseurl->param('tag', $tag);
 172              }
 173  
 174              if (is_null($typeid)) {
 175                  $baseurl->param('limit', $limitnum);
 176                  if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
 177                      $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
 178                  }
 179                  $firstpage = new \moodle_url($baseurl, ['from' => 0]);
 180                  $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
 181                  $lastpage = new \moodle_url($baseurl, ['from' > $limitlast]);
 182                  if (($limitcurrent > 0) && (!$outofrange)) {
 183                      $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
 184                  }
 185              } else {
 186                  $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]);
 187                  if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
 188                      $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
 189                  }
 190                  $firstpage = new \moodle_url($baseurl, ['from' => 0]);
 191                  $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
 192                  $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]);
 193                  if (($limitcurrent > 0) && (!$outofrange)) {
 194                      $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
 195                  }
 196              }
 197          }
 198  
 199          $jsonitems = [];
 200          $endpoint = parent::get_endpoint();
 201          foreach ($items as $item) {
 202              array_push($jsonitems, gradebookservices::item_for_json($item, $endpoint, $typeid));
 203          }
 204  
 205          if (isset($canonicalpage) && ($canonicalpage)) {
 206              $links = 'Link: <' . $firstpage->out() . '>; rel=“first”';
 207              if (!is_null($prevpage)) {
 208                  $links .= ', <' . $prevpage->out() . '>; rel=“prev”';
 209              }
 210              $links .= ', <' . $canonicalpage->out(). '>; rel=“canonical”';
 211              if (!is_null($nextpage)) {
 212                  $links .= ', <' . $nextpage->out() . '>; rel=“next”';
 213              }
 214              $links .= ', <' . $lastpage->out() . '>; rel=“last”';
 215              $response->add_additional_header($links);
 216          }
 217          return json_encode($jsonitems);
 218      }
 219  
 220      /**
 221       * Generate the JSON for a POST request.
 222       *
 223       * @param string $body POST body
 224       * @param string $contextid Course ID
 225       * @param string $typeid
 226       *
 227       * @return string
 228       * @throws \Exception
 229       */
 230      private function get_json_for_post_request($body, $contextid, $typeid) {
 231          global $CFG, $DB;
 232  
 233          $json = json_decode($body);
 234          if (empty($json) ||
 235                  !isset($json->scoreMaximum) ||
 236                  !isset($json->label)) {
 237              throw new \Exception('No label or Score Maximum', 400);
 238          }
 239          if (is_numeric($json->scoreMaximum)) {
 240              $max = $json->scoreMaximum;
 241          } else {
 242              throw new \Exception(null, 400);
 243          }
 244          require_once($CFG->libdir.'/gradelib.php');
 245          $resourceid = (isset($json->resourceId)) ? $json->resourceId : '';
 246          $ltilinkid = (isset($json->resourceLinkId)) ? $json->resourceLinkId : null;
 247          if ($ltilinkid == null) {
 248              $ltilinkid = (isset($json->ltiLinkId)) ? $json->ltiLinkId : null;
 249          }
 250          if ($ltilinkid != null) {
 251              if (is_null($typeid)) {
 252                  if (!gradebookservices::check_lti_id($ltilinkid, $contextid, $this->get_service()->get_tool_proxy()->id)) {
 253                      throw new \Exception(null, 403);
 254                  }
 255              } else {
 256                  if (!gradebookservices::check_lti_1x_id($ltilinkid, $contextid, $typeid)) {
 257                      throw new \Exception(null, 403);
 258                  }
 259              }
 260          }
 261          $tag = (isset($json->tag)) ? $json->tag : '';
 262          if (is_null($typeid)) {
 263              $toolproxyid = $this->get_service()->get_tool_proxy()->id;
 264              $baseurl = null;
 265          } else {
 266              $toolproxyid = null;
 267              $baseurl = lti_get_type_type_config($typeid)->lti_toolurl;
 268          }
 269          $gradebookservices = new gradebookservices();
 270          $id = $gradebookservices->add_standalone_lineitem($contextid, $json->label,
 271              $max, $baseurl, $ltilinkid, $resourceid, $tag, $typeid, $toolproxyid);
 272          if (is_null($typeid)) {
 273              $json->id = parent::get_endpoint() . "/{$id}/lineitem";
 274          } else {
 275              $json->id = parent::get_endpoint() . "/{$id}/lineitem?type_id={$typeid}";
 276          }
 277          return json_encode($json, JSON_UNESCAPED_SLASHES);
 278      }
 279  
 280      /**
 281       * Parse a value for custom parameter substitution variables.
 282       *
 283       * @param string $value String to be parsed
 284       *
 285       * @return string
 286       */
 287      public function parse_value($value) {
 288          global $COURSE;
 289  
 290          if (strpos($value, '$LineItems.url') !== false) {
 291              $this->params['context_id'] = $COURSE->id;
 292              $query = '';
 293              if (($tool = $this->get_service()->get_type())) {
 294                  $query = "?type_id={$tool->id}";
 295              }
 296              $value = str_replace('$LineItems.url', parent::get_endpoint() . $query, $value);
 297          }
 298  
 299          return $value;
 300  
 301      }
 302  }