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

   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 LTI Gradebook Services
  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\service;
  27  
  28  use ltiservice_gradebookservices\local\resources\lineitem;
  29  use ltiservice_gradebookservices\local\resources\lineitems;
  30  use ltiservice_gradebookservices\local\resources\results;
  31  use ltiservice_gradebookservices\local\resources\scores;
  32  use mod_lti\local\ltiservice\resource_base;
  33  use mod_lti\local\ltiservice\service_base;
  34  use moodle_url;
  35  
  36  defined('MOODLE_INTERNAL') || die();
  37  
  38  global $CFG;
  39  require_once($CFG->dirroot . '/mod/lti/locallib.php');
  40  
  41  /**
  42   * A service implementing LTI Gradebook Services.
  43   *
  44   * @package    ltiservice_gradebookservices
  45   * @copyright  2017 Cengage Learning http://www.cengage.com
  46   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47   */
  48  class gradebookservices extends service_base {
  49  
  50      /** Read-only access to Gradebook services */
  51      const GRADEBOOKSERVICES_READ = 1;
  52      /** Full access to Gradebook services */
  53      const GRADEBOOKSERVICES_FULL = 2;
  54      /** Scope for full access to Lineitem service */
  55      const SCOPE_GRADEBOOKSERVICES_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
  56      /** Scope for full access to Lineitem service */
  57      const SCOPE_GRADEBOOKSERVICES_LINEITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
  58      /** Scope for access to Result service */
  59      const SCOPE_GRADEBOOKSERVICES_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
  60      /** Scope for access to Score service */
  61      const SCOPE_GRADEBOOKSERVICES_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
  62  
  63  
  64      /**
  65       * Class constructor.
  66       */
  67      public function __construct() {
  68  
  69          parent::__construct();
  70          $this->id = 'gradebookservices';
  71          $this->name = get_string($this->get_component_id(), $this->get_component_id());
  72  
  73      }
  74  
  75      /**
  76       * Get the resources for this service.
  77       *
  78       * @return resource_base[]
  79       */
  80      public function get_resources() {
  81  
  82          // The containers should be ordered in the array after their elements.
  83          // Lineitems should be after lineitem.
  84          if (empty($this->resources)) {
  85              $this->resources = array();
  86              $this->resources[] = new lineitem($this);
  87              $this->resources[] = new lineitems($this);
  88              $this->resources[] = new results($this);
  89              $this->resources[] = new scores($this);
  90          }
  91  
  92          return $this->resources;
  93      }
  94  
  95      /**
  96       * Get the scope(s) permitted for this service.
  97       *
  98       * @return array
  99       */
 100      public function get_permitted_scopes() {
 101  
 102          $scopes = array();
 103          $ok = !empty($this->get_type());
 104          if ($ok && isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) {
 105              if (!empty($setting = $this->get_typeconfig()['ltiservice_gradesynchronization'])) {
 106                  $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
 107                  $scopes[] = self::SCOPE_GRADEBOOKSERVICES_RESULT_READ;
 108                  $scopes[] = self::SCOPE_GRADEBOOKSERVICES_SCORE;
 109                  if ($setting == self::GRADEBOOKSERVICES_FULL) {
 110                      $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM;
 111                  }
 112              }
 113          }
 114  
 115          return $scopes;
 116  
 117      }
 118  
 119      /**
 120       * Get the scopes defined by this service.
 121       *
 122       * @return array
 123       */
 124      public function get_scopes() {
 125          return [self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ, self::SCOPE_GRADEBOOKSERVICES_RESULT_READ,
 126              self::SCOPE_GRADEBOOKSERVICES_SCORE, self::SCOPE_GRADEBOOKSERVICES_LINEITEM];
 127      }
 128  
 129      /**
 130       * Adds form elements for gradebook sync add/edit page.
 131       *
 132       * @param \MoodleQuickForm $mform Moodle quickform object definition
 133       */
 134      public function get_configuration_options(&$mform) {
 135  
 136          $selectelementname = 'ltiservice_gradesynchronization';
 137          $identifier = 'grade_synchronization';
 138          $options = [
 139              get_string('nevergs', $this->get_component_id()),
 140              get_string('partialgs', $this->get_component_id()),
 141              get_string('alwaysgs', $this->get_component_id())
 142          ];
 143  
 144          $mform->addElement('select', $selectelementname, get_string($identifier, $this->get_component_id()), $options);
 145          $mform->setType($selectelementname, 'int');
 146          $mform->setDefault($selectelementname, 0);
 147          $mform->addHelpButton($selectelementname, $identifier, $this->get_component_id());
 148      }
 149  
 150      /**
 151       * For submission review, if there is a dedicated URL, use it as the target link.
 152       *
 153       * @param string $messagetype message type for this launch
 154       * @param string $targetlinkuri current target link uri
 155       * @param string|null $customstr concatenated list of custom parameters
 156       * @param int $courseid
 157       * @param null|object $lti LTI Instance.
 158       *
 159       * @return array containing the target link URL and the custom params string to use.
 160       */
 161      public function override_endpoint(string $messagetype, string $targetlinkuri, ?string $customstr, int $courseid,
 162              ?object $lti = null): array {
 163          global $DB;
 164          if ($messagetype == 'LtiSubmissionReviewRequest' && isset($lti->id)) {
 165              $conditions = array('courseid' => $courseid, 'ltilinkid' => $lti->id);
 166              $coupledlineitems = $DB->get_records('ltiservice_gradebookservices', $conditions);
 167              if (count($coupledlineitems) == 1) {
 168                  $item = reset($coupledlineitems);
 169                  $url = $item->subreviewurl;
 170                  $subreviewparams = $item->subreviewparams;
 171                  if (!empty($url) && $url != 'DEFAULT') {
 172                      $targetlinkuri = $url;
 173                  }
 174                  if (!empty($subreviewparams)) {
 175                      if (!empty($customstr)) {
 176                          $customstr .= "\n{$subreviewparams}";
 177                      } else {
 178                          $customstr = $subreviewparams;
 179                      }
 180                  }
 181              }
 182          }
 183          return [$targetlinkuri, $customstr];
 184      }
 185  
 186      /**
 187       * Return an array of key/claim mapping allowing LTI 1.1 custom parameters
 188       * to be transformed to LTI 1.3 claims.
 189       *
 190       * @return array Key/value pairs of params to claim mapping.
 191       */
 192      public function get_jwt_claim_mappings(): array {
 193          return [
 194              'custom_gradebookservices_scope' => [
 195                  'suffix' => 'ags',
 196                  'group' => 'endpoint',
 197                  'claim' => 'scope',
 198                  'isarray' => true
 199              ],
 200              'custom_lineitems_url' => [
 201                  'suffix' => 'ags',
 202                  'group' => 'endpoint',
 203                  'claim' => 'lineitems',
 204                  'isarray' => false
 205              ],
 206              'custom_lineitem_url' => [
 207                  'suffix' => 'ags',
 208                  'group' => 'endpoint',
 209                  'claim' => 'lineitem',
 210                  'isarray' => false
 211              ],
 212              'custom_results_url' => [
 213                  'suffix' => 'ags',
 214                  'group' => 'endpoint',
 215                  'claim' => 'results',
 216                  'isarray' => false
 217              ],
 218              'custom_result_url' => [
 219                  'suffix' => 'ags',
 220                  'group' => 'endpoint',
 221                  'claim' => 'result',
 222                  'isarray' => false
 223              ],
 224              'custom_scores_url' => [
 225                  'suffix' => 'ags',
 226                  'group' => 'endpoint',
 227                  'claim' => 'scores',
 228                  'isarray' => false
 229              ],
 230              'custom_score_url' => [
 231                  'suffix' => 'ags',
 232                  'group' => 'endpoint',
 233                  'claim' => 'score',
 234                  'isarray' => false
 235              ]
 236          ];
 237      }
 238  
 239      /**
 240       * Return an array of key/values to add to the launch parameters.
 241       *
 242       * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'.
 243       * @param string $courseid the course id.
 244       * @param object $user The user id.
 245       * @param string $typeid The tool lti type id.
 246       * @param string $modlti The id of the lti activity.
 247       *
 248       * The type is passed to check the configuration
 249       * and not return parameters for services not used.
 250       *
 251       * @return array of key/value pairs to add as launch parameters.
 252       */
 253      public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) {
 254          global $DB;
 255          $launchparameters = array();
 256          $this->set_type(lti_get_type($typeid));
 257          $this->set_typeconfig(lti_get_type_config($typeid));
 258          // Only inject parameters if the service is enabled for this tool.
 259          if (isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) {
 260              if ($this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_READ ||
 261                      $this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_FULL) {
 262                  // Check for used in context is only needed because there is no explicit site tool - course relation.
 263                  if ($this->is_allowed_in_context($typeid, $courseid)) {
 264                      $id = null;
 265                      if (!is_null($modlti)) {
 266                          $conditions = array('courseid' => $courseid, 'itemtype' => 'mod',
 267                                  'itemmodule' => 'lti', 'iteminstance' => $modlti);
 268  
 269                          $coupledlineitems = $DB->get_records('grade_items', $conditions);
 270                          $conditionsgbs = array('courseid' => $courseid, 'ltilinkid' => $modlti);
 271                          $lineitemsgbs = $DB->get_records('ltiservice_gradebookservices', $conditionsgbs);
 272                          // If a link has more that one attached grade items, per spec we do not populate line item url.
 273                          if (count($lineitemsgbs) == 1) {
 274                              $id = reset($lineitemsgbs)->gradeitemid;
 275                          }
 276                          if (count($lineitemsgbs) < 2 && count($coupledlineitems) == 1) {
 277                              $coupledid = reset($coupledlineitems)->id;
 278                              if (!is_null($id) && $id != $coupledid) {
 279                                  $id = null;
 280                              } else {
 281                                  $id = $coupledid;
 282                              }
 283                          }
 284                      }
 285                      $launchparameters['gradebookservices_scope'] = implode(',', $this->get_permitted_scopes());
 286                      $launchparameters['lineitems_url'] = '$LineItems.url';
 287                      if (!is_null($id)) {
 288                          $launchparameters['lineitem_url'] = '$LineItem.url';
 289                      }
 290                  }
 291              }
 292          }
 293          return $launchparameters;
 294      }
 295  
 296      /**
 297       * Fetch the lineitem instances.
 298       *
 299       * @param string $courseid ID of course
 300       * @param string $resourceid Resource identifier used for filtering, may be null
 301       * @param string $ltilinkid Resource Link identifier used for filtering, may be null
 302       * @param string $tag
 303       * @param int $limitfrom Offset for the first line item to include in a paged set
 304       * @param int $limitnum Maximum number of line items to include in the paged set
 305       * @param string $typeid
 306       *
 307       * @return array
 308       * @throws \Exception
 309       */
 310      public function get_lineitems($courseid, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $typeid) {
 311          global $DB;
 312  
 313          // Select all lti potential linetiems in site.
 314          $params = array('courseid' => $courseid);
 315  
 316          $sql = "SELECT i.*
 317                    FROM {grade_items} i
 318                   WHERE (i.courseid = :courseid)
 319                 ORDER BY i.id";
 320          $lineitems = $DB->get_records_sql($sql, $params);
 321  
 322          // For each one, check the gbs id, and check that toolproxy matches. If so, add the
 323          // tag to the result and add it to a final results array.
 324          $lineitemstoreturn = array();
 325          $lineitemsandtotalcount = array();
 326          if ($lineitems) {
 327              foreach ($lineitems as $lineitem) {
 328                  $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($lineitem->id);
 329                  if ($gbs && (!isset($tag) || (isset($tag) && $gbs->tag == $tag))
 330                          && (!isset($ltilinkid) || (isset($ltilinkid) && $gbs->ltilinkid == $ltilinkid))
 331                          && (!isset($resourceid) || (isset($resourceid) && $gbs->resourceid == $resourceid))) {
 332                      if (is_null($typeid)) {
 333                          if ($this->get_tool_proxy()->id == $gbs->toolproxyid) {
 334                              array_push($lineitemstoreturn, $lineitem);
 335                          }
 336                      } else {
 337                          if ($typeid == $gbs->typeid) {
 338                              array_push($lineitemstoreturn, $lineitem);
 339                          }
 340                      }
 341                  } else if (($lineitem->itemtype == 'mod' && $lineitem->itemmodule == 'lti'
 342                          && !isset($resourceid) && !isset($tag)
 343                          && (!isset($ltilinkid) || (isset($ltilinkid)
 344                          && $lineitem->iteminstance == $ltilinkid)))) {
 345                      // We will need to check if the activity related belongs to our tool proxy.
 346                      $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance));
 347                      if (($ltiactivity) && (isset($ltiactivity->typeid))) {
 348                          if ($ltiactivity->typeid != 0) {
 349                              $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid));
 350                          } else {
 351                              $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid);
 352                              if (!$tool) {
 353                                  $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid);
 354                              }
 355                          }
 356                          if (is_null($typeid)) {
 357                              if (($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid)) {
 358                                  array_push($lineitemstoreturn, $lineitem);
 359                              }
 360                          } else {
 361                              if (($tool) && ($tool->id == $typeid)) {
 362                                  array_push($lineitemstoreturn, $lineitem);
 363                              }
 364                          }
 365                      }
 366                  }
 367              }
 368              $lineitemsandtotalcount = array();
 369              array_push($lineitemsandtotalcount, count($lineitemstoreturn));
 370              // Return the right array based in the paging parameters limit and from.
 371              if (($limitnum) && ($limitnum > 0)) {
 372                  $lineitemstoreturn = array_slice($lineitemstoreturn, $limitfrom, $limitnum);
 373              }
 374              array_push($lineitemsandtotalcount, $lineitemstoreturn);
 375          }
 376          return $lineitemsandtotalcount;
 377      }
 378  
 379      /**
 380       * Fetch a lineitem instance.
 381       *
 382       * Returns the lineitem instance if found, otherwise false.
 383       *
 384       * @param string $courseid ID of course
 385       * @param string $itemid ID of lineitem
 386       * @param string $typeid
 387       *
 388       * @return \ltiservice_gradebookservices\local\resources\lineitem|bool
 389       */
 390      public function get_lineitem($courseid, $itemid, $typeid) {
 391          global $DB, $CFG;
 392  
 393          require_once($CFG->libdir . '/gradelib.php');
 394          $lineitem = \grade_item::fetch(array('id' => $itemid));
 395          if ($lineitem) {
 396              $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($itemid);
 397              if (!$gbs) {
 398                  // We will need to check if the activity related belongs to our tool proxy.
 399                  $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance));
 400                  if (($ltiactivity) && (isset($ltiactivity->typeid))) {
 401                      if ($ltiactivity->typeid != 0) {
 402                          $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid));
 403                      } else {
 404                          $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid);
 405                          if (!$tool) {
 406                              $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid);
 407                          }
 408                      }
 409                      if (is_null($typeid)) {
 410                          if (!(($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid))) {
 411                              return false;
 412                          }
 413                      } else {
 414                          if (!(($tool) && ($tool->id == $typeid))) {
 415                              return false;
 416                          }
 417                      }
 418                  } else {
 419                      return false;
 420                  }
 421              }
 422          }
 423          return $lineitem;
 424      }
 425  
 426      /**
 427       * Adds a decoupled (standalone) line item.
 428       * Decoupled line items are not directly attached to
 429       * an lti instance activity. They are recorded in
 430       * the gradebook as manual activities and the
 431       * gradebookservices is used to associate that manual column
 432       * with the tool in addition to storing the LTI related
 433       * metadata (resource id, tag).
 434       *
 435       * @param string $courseid ID of course
 436       * @param string $label label of lineitem
 437       * @param float $maximumscore maximum score of lineitem
 438       * @param string $baseurl
 439       * @param int|null $ltilinkid id of lti instance this line item is associated with
 440       * @param string|null $resourceid resource id of lineitem
 441       * @param string|null $tag tag of lineitem
 442       * @param int $typeid lti type to which this line item is associated with
 443       * @param int|null $toolproxyid lti2 tool proxy to which this lineitem is associated to
 444       *
 445       * @return int id of the created gradeitem
 446       */
 447      public function add_standalone_lineitem(string $courseid, string $label, float $maximumscore,
 448              string $baseurl, ?int $ltilinkid, ?string $resourceid, ?string $tag, int $typeid,
 449              int $toolproxyid = null) : int {
 450          global $DB;
 451          $params = array();
 452          $params['itemname'] = $label;
 453          $params['gradetype'] = GRADE_TYPE_VALUE;
 454          $params['grademax']  = $maximumscore;
 455          $params['grademin']  = 0;
 456          $item = new \grade_item(array('id' => 0, 'courseid' => $courseid));
 457          \grade_item::set_properties($item, $params);
 458          $item->itemtype = 'manual';
 459          $item->grademax = $maximumscore;
 460          $id = $item->insert('mod/ltiservice_gradebookservices');
 461          $DB->insert_record('ltiservice_gradebookservices', (object)array(
 462                  'gradeitemid' => $id,
 463                  'courseid' => $courseid,
 464                  'toolproxyid' => $toolproxyid,
 465                  'typeid' => $typeid,
 466                  'baseurl' => $baseurl,
 467                  'ltilinkid' => $ltilinkid,
 468                  'resourceid' => $resourceid,
 469                  'tag' => $tag
 470          ));
 471          return $id;
 472      }
 473  
 474      /**
 475       * Set a grade item.
 476       *
 477       * @param object $gradeitem Grade Item record
 478       * @param object $score Result object
 479       * @param int $userid User ID
 480       *
 481       * @throws \Exception
 482       * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
 483       * @see gradebookservices::save_grade_item($gradeitem, $score, $userid)
 484       */
 485      public static function save_score($gradeitem, $score, $userid) {
 486          $service = new gradebookservices();
 487          $service->save_grade_item($gradeitem, $score, $userid);
 488      }
 489  
 490      /**
 491       * Saves a score received from the LTI tool.
 492       *
 493       * @param object $gradeitem Grade Item record
 494       * @param object $score Result object
 495       * @param int $userid User ID
 496       *
 497       * @throws \Exception
 498       */
 499      public function save_grade_item($gradeitem, $score, $userid) {
 500          global $DB, $CFG;
 501          $source = 'mod' . $this->get_component_id();
 502          if ($DB->get_record('user', array('id' => $userid)) === false) {
 503              throw new \Exception(null, 400);
 504          }
 505          require_once($CFG->libdir . '/gradelib.php');
 506          $finalgrade = null;
 507          $timemodified = null;
 508          if (isset($score->scoreGiven)) {
 509              $finalgrade = grade_floatval($score->scoreGiven);
 510              $max = 1;
 511              if (isset($score->scoreMaximum)) {
 512                  $max = $score->scoreMaximum;
 513              }
 514              if (!is_null($max) && grade_floats_different($max, $gradeitem->grademax) && grade_floats_different($max, 0.0)) {
 515                  // Rescale to match the grade item maximum.
 516                  $finalgrade = grade_floatval($finalgrade * $gradeitem->grademax / $max);
 517              }
 518              if (isset($score->timestamp)) {
 519                  $timemodified = strtotime($score->timestamp);
 520              } else {
 521                  $timemodified = time();
 522              }
 523          }
 524          $feedbackformat = FORMAT_MOODLE;
 525          $feedback = null;
 526          if (!empty($score->comment)) {
 527              $feedback = $score->comment;
 528              $feedbackformat = FORMAT_PLAIN;
 529          }
 530  
 531          if ($gradeitem->is_manual_item()) {
 532              $result = $gradeitem->update_final_grade($userid, $finalgrade, null, $feedback, FORMAT_PLAIN, null, $timemodified);
 533          } else {
 534              if (!$grade = \grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $userid))) {
 535                  $grade = new \grade_grade();
 536                  $grade->userid = $userid;
 537                  $grade->itemid = $gradeitem->id;
 538              }
 539              $grade->rawgrademax = $score->scoreMaximum;
 540              $grade->timemodified = $timemodified;
 541              $grade->feedbackformat = $feedbackformat;
 542              $grade->feedback = $feedback;
 543              $grade->rawgrade = $finalgrade;
 544              $status = grade_update($source, $gradeitem->courseid,
 545                  $gradeitem->itemtype, $gradeitem->itemmodule,
 546                  $gradeitem->iteminstance, $gradeitem->itemnumber, $grade);
 547  
 548              $result = ($status == GRADE_UPDATE_OK);
 549          }
 550          if (!$result) {
 551              debugging("failed to save score for item ".$gradeitem->id." and user ".$grade->userid);
 552              throw new \Exception(null, 500);
 553          }
 554  
 555      }
 556  
 557      /**
 558       * Get the json object representation of the grade item
 559       *
 560       * @param object $item Grade Item record
 561       * @param string $endpoint Endpoint for lineitems container request
 562       * @param string $typeid
 563       *
 564       * @return object
 565       */
 566      public static function item_for_json($item, $endpoint, $typeid) {
 567  
 568          $lineitem = new \stdClass();
 569          if (is_null($typeid)) {
 570              $typeidstring = "";
 571          } else {
 572              $typeidstring = "?type_id={$typeid}";
 573          }
 574          $lineitem->id = "{$endpoint}/{$item->id}/lineitem" . $typeidstring;
 575          $lineitem->label = $item->itemname;
 576          $lineitem->scoreMaximum = floatval($item->grademax);
 577          $gbs = self::find_ltiservice_gradebookservice_for_lineitem($item->id);
 578          if ($gbs) {
 579              $lineitem->resourceId = (!empty($gbs->resourceid)) ? $gbs->resourceid : '';
 580              $lineitem->tag = (!empty($gbs->tag)) ? $gbs->tag : '';
 581              if (isset($gbs->ltilinkid)) {
 582                  $lineitem->resourceLinkId = strval($gbs->ltilinkid);
 583                  $lineitem->ltiLinkId = strval($gbs->ltilinkid);
 584              }
 585              if (!empty($gbs->subreviewurl)) {
 586                  $submissionreview = new \stdClass();
 587                  if ($gbs->subreviewurl != 'DEFAULT') {
 588                      $submissionreview->url = $gbs->subreviewurl;
 589                  }
 590                  if (!empty($gbs->subreviewparams)) {
 591                      $submissionreview->custom = lti_split_parameters($gbs->subreviewparams);
 592                  }
 593                  $lineitem->submissionReview = $submissionreview;
 594              }
 595          } else {
 596              $lineitem->tag = '';
 597              if (isset($item->iteminstance)) {
 598                  $lineitem->resourceLinkId = strval($item->iteminstance);
 599                  $lineitem->ltiLinkId = strval($item->iteminstance);
 600              }
 601          }
 602  
 603          return $lineitem;
 604  
 605      }
 606  
 607      /**
 608       * Get the object matching the JSON representation of the result.
 609       *
 610       * @param object  $grade              Grade record
 611       * @param string  $endpoint           Endpoint for lineitem
 612       * @param int  $typeid                The id of the type to include in the result url.
 613       *
 614       * @return object
 615       */
 616      public static function result_for_json($grade, $endpoint, $typeid) {
 617  
 618          if (is_null($typeid)) {
 619              $id = "{$endpoint}/results?user_id={$grade->userid}";
 620          } else {
 621              $id = "{$endpoint}/results?type_id={$typeid}&user_id={$grade->userid}";
 622          }
 623          $result = new \stdClass();
 624          $result->id = $id;
 625          $result->userId = $grade->userid;
 626          if (!empty($grade->finalgrade)) {
 627              $result->resultScore = floatval($grade->finalgrade);
 628              $result->resultMaximum = floatval($grade->rawgrademax);
 629              if (!empty($grade->feedback)) {
 630                  $result->comment = $grade->feedback;
 631              }
 632              if (is_null($typeid)) {
 633                  $result->scoreOf = $endpoint;
 634              } else {
 635                  $result->scoreOf = "{$endpoint}?type_id={$typeid}";
 636              }
 637              $result->timestamp = date('c', $grade->timemodified);
 638          }
 639          return $result;
 640      }
 641  
 642      /**
 643       * Check if an LTI id is valid.
 644       *
 645       * @param string $linkid             The lti id
 646       * @param string  $course            The course
 647       * @param string  $toolproxy         The tool proxy id
 648       *
 649       * @return boolean
 650       */
 651      public static function check_lti_id($linkid, $course, $toolproxy) {
 652          global $DB;
 653          // Check if lti type is zero or not (comes from a backup).
 654          $sqlparams1 = array();
 655          $sqlparams1['linkid'] = $linkid;
 656          $sqlparams1['course'] = $course;
 657          $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course));
 658          if ($ltiactivity->typeid == 0) {
 659              $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course);
 660              if (!$tool) {
 661                  $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course);
 662              }
 663              return (($tool) && ($toolproxy == $tool->toolproxyid));
 664          } else {
 665              $sqlparams2 = array();
 666              $sqlparams2['linkid'] = $linkid;
 667              $sqlparams2['course'] = $course;
 668              $sqlparams2['toolproxy'] = $toolproxy;
 669              $sql = 'SELECT lti.*
 670                        FROM {lti} lti
 671                  INNER JOIN {lti_types} typ ON lti.typeid = typ.id
 672                       WHERE lti.id = ?
 673                             AND lti.course = ?
 674                             AND typ.toolproxyid = ?';
 675              return $DB->record_exists_sql($sql, $sqlparams2);
 676          }
 677      }
 678  
 679      /**
 680       * Check if an LTI id is valid when we are in a LTI 1.x case
 681       *
 682       * @param string $linkid             The lti id
 683       * @param string  $course            The course
 684       * @param string  $typeid            The lti type id
 685       *
 686       * @return boolean
 687       */
 688      public static function check_lti_1x_id($linkid, $course, $typeid) {
 689          global $DB;
 690          // Check if lti type is zero or not (comes from a backup).
 691          $sqlparams1 = array();
 692          $sqlparams1['linkid'] = $linkid;
 693          $sqlparams1['course'] = $course;
 694          $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course));
 695          if ($ltiactivity) {
 696              if ($ltiactivity->typeid == 0) {
 697                  $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course);
 698                  if (!$tool) {
 699                      $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course);
 700                  }
 701                  return (($tool) && ($typeid == $tool->id));
 702              } else {
 703                  $sqlparams2 = array();
 704                  $sqlparams2['linkid'] = $linkid;
 705                  $sqlparams2['course'] = $course;
 706                  $sqlparams2['typeid'] = $typeid;
 707                  $sql = 'SELECT lti.*
 708                            FROM {lti} lti
 709                      INNER JOIN {lti_types} typ ON lti.typeid = typ.id
 710                           WHERE lti.id = ?
 711                                 AND lti.course = ?
 712                                 AND typ.id = ?';
 713                  return $DB->record_exists_sql($sql, $sqlparams2);
 714              }
 715          } else {
 716              return false;
 717          }
 718      }
 719  
 720      /**
 721       * Updates the tag, resourceid and submission review values for a grade item coupled to an lti link instance.
 722       *
 723       * @param object $ltiinstance The lti instance to which the grade item is coupled to
 724       * @param string|null $resourceid The resourceid to apply to the lineitem. If empty string which will be stored as null.
 725       * @param string|null $tag The tag to apply to the lineitem. If empty string which will be stored as null.
 726       * @param moodle_url|null $subreviewurl The submission review target link URL
 727       * @param string|null $subreviewparams The submission review custom parameters.
 728       *
 729       */
 730      public static function update_coupled_gradebookservices(object $ltiinstance,
 731              ?string $resourceid, ?string $tag, ?\moodle_url $subreviewurl, ?string $subreviewparams) : void {
 732          global $DB;
 733  
 734          if ($ltiinstance && $ltiinstance->typeid) {
 735              $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $ltiinstance->id));
 736              if ($gradeitem) {
 737                  $resourceid = (isset($resourceid) && empty(trim($resourceid))) ? null : $resourceid;
 738                  $subreviewurlstr = $subreviewurl ? $subreviewurl->out(false) : null;
 739                  $tag = (isset($tag) && empty(trim($tag))) ? null : $tag;
 740                  $gbs = self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id);
 741                  if ($gbs) {
 742                      $gbs->resourceid = $resourceid;
 743                      $gbs->tag = $tag;
 744                      $gbs->subreviewurl = $subreviewurlstr;
 745                      $gbs->subreviewparams = $subreviewparams;
 746                      $DB->update_record('ltiservice_gradebookservices', $gbs);
 747                  } else {
 748                      $baseurl = lti_get_type_type_config($ltiinstance->typeid)->lti_toolurl;
 749                      $DB->insert_record('ltiservice_gradebookservices', (object)array(
 750                          'gradeitemid' => $gradeitem->id,
 751                          'courseid' => $gradeitem->courseid,
 752                          'typeid' => $ltiinstance->typeid,
 753                          'baseurl' => $baseurl,
 754                          'ltilinkid' => $ltiinstance->id,
 755                          'resourceid' => $resourceid,
 756                          'tag' => $tag,
 757                          'subreviewurl' => $subreviewurlstr,
 758                          'subreviewparams' => $subreviewparams
 759                      ));
 760                  }
 761              }
 762          }
 763      }
 764  
 765      /**
 766       * Called when a new LTI Instance is added.
 767       *
 768       * @param object $lti LTI Instance.
 769       */
 770      public function instance_added(object $lti): void {
 771          self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null,
 772              isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null,
 773              $lti->lineitemsubreviewparams ?? null);
 774      }
 775  
 776      /**
 777       * Called when a new LTI Instance is updated.
 778       *
 779       * @param object $lti LTI Instance.
 780       */
 781      public function instance_updated(object $lti): void {
 782          self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null,
 783              isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null,
 784              $lti->lineitemsubreviewparams ?? null);
 785      }
 786  
 787      /**
 788       * Set the form data when displaying the LTI Instance form.
 789       *
 790       * @param object $defaultvalues Default form values.
 791       */
 792      public function set_instance_form_values(object $defaultvalues): void {
 793          $defaultvalues->lineitemresourceid = '';
 794          $defaultvalues->lineitemtag = '';
 795          $defaultvalues->subreviewurl = '';
 796          $defaultvalues->subreviewparams = '';
 797          if (is_object($defaultvalues) && $defaultvalues->instance) {
 798              $gbs = self::find_ltiservice_gradebookservice_for_lti($defaultvalues->instance);
 799              if ($gbs) {
 800                  $defaultvalues->lineitemresourceid = $gbs->resourceid;
 801                  $defaultvalues->lineitemtag = $gbs->tag;
 802                  $defaultvalues->lineitemsubreviewurl = $gbs->subreviewurl;
 803                  $defaultvalues->lineitemsubreviewparams = $gbs->subreviewparams;
 804              }
 805          }
 806      }
 807  
 808      /**
 809       * Deletes orphaned rows from the 'ltiservice_gradebookservices' table.
 810       *
 811       * Sometimes, if a gradebook entry is deleted and it was a lineitem
 812       * the row in the table ltiservice_gradebookservices can become an orphan
 813       * This method will clean these orphans. It will happens based on a task
 814       * because it is not urgent and we don't want to slow the service
 815       */
 816      public static function delete_orphans_ltiservice_gradebookservices_rows() {
 817          global $DB;
 818  
 819          $sql = "DELETE
 820                    FROM {ltiservice_gradebookservices}
 821                   WHERE gradeitemid NOT IN (SELECT id
 822                                               FROM {grade_items} gi)";
 823          $DB->execute($sql);
 824      }
 825  
 826      /**
 827       * Check if a user can be graded in a course
 828       *
 829       * @param int $courseid The course
 830       * @param int $userid The user
 831       * @return bool
 832       */
 833      public static function is_user_gradable_in_course($courseid, $userid) {
 834          global $CFG;
 835  
 836          $gradableuser = false;
 837          $coursecontext = \context_course::instance($courseid);
 838          if (is_enrolled($coursecontext, $userid, '', false)) {
 839              $roles = get_user_roles($coursecontext, $userid);
 840              $gradebookroles = explode(',', $CFG->gradebookroles);
 841              foreach ($roles as $role) {
 842                  foreach ($gradebookroles as $gradebookrole) {
 843                      if ($role->roleid === $gradebookrole) {
 844                          $gradableuser = true;
 845                      }
 846                  }
 847              }
 848          }
 849  
 850          return $gradableuser;
 851      }
 852  
 853      /**
 854       * Find the right element in the ltiservice_gradebookservice table for an lti instance
 855       *
 856       * @param string $instanceid The LTI module instance id
 857       * @return object gradebookservice for this line item
 858       */
 859      public static function find_ltiservice_gradebookservice_for_lti($instanceid) {
 860          global $DB;
 861  
 862          if ($instanceid) {
 863              $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $instanceid));
 864              if ($gradeitem) {
 865                  return self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id);
 866              }
 867          }
 868      }
 869  
 870      /**
 871       * Find the right element in the ltiservice_gradebookservice table for a lineitem
 872       *
 873       * @param string $lineitemid The lineitem (gradeitem) id
 874       * @return object gradebookservice if it exists
 875       */
 876      public static function find_ltiservice_gradebookservice_for_lineitem($lineitemid) {
 877          global $DB;
 878          if ($lineitemid) {
 879              return $DB->get_record('ltiservice_gradebookservices',
 880                      array('gradeitemid' => $lineitemid));
 881          }
 882      }
 883  
 884      /**
 885       * Validates specific ISO 8601 format of the timestamps.
 886       *
 887       * @param string $date The timestamp to check.
 888       * @return boolean true or false if the date matches the format.
 889       *
 890       */
 891      public static function validate_iso8601_date($date) {
 892          if (preg_match('/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])' .
 893                  '(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))' .
 894                  '([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)' .
 895                  '?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/', $date) > 0) {
 896              return true;
 897          } else {
 898              return false;
 899          }
 900      }
 901  }